diff --git a/.env.example b/.env.example index a05683ff7..cc70644c1 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ PORT=5000 LNBITS_SITE_TITLE=LNbits LNBITS_ALLOWED_USERS="" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" -LNBITS_DATA_FOLDER="." +LNBITS_DATA_FOLDER="./data" LNBITS_DISABLED_EXTENSIONS="amilk" LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" @@ -44,7 +44,9 @@ LND_REST_MACAROON="HEXSTRING" # LNPayWallet LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ +# Secret API Key under developers tab LNPAY_API_KEY=LNPAY_API_KEY +# Wallet Admin in Wallet Access Keys LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY # LntxbotWallet diff --git a/Pipfile b/Pipfile index ae59e0b12..addd84b62 100644 --- a/Pipfile +++ b/Pipfile @@ -35,3 +35,4 @@ pytest = "*" pytest-cov = "*" mypy = "latest" pytest-trio = "*" +trio-typing = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 0dc59dc54..81b4d6898 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e12af74353e8bea3f97bf2aea16a1ba0a6e4c3a08042ce7368187a06e7791e2c" + "sha256": "8c4056a80c682fac834266c11892573ce53807226c0810e4564976656ea5ff45" }, "pipfile-spec": 6, "requires": { @@ -18,10 +18,19 @@ "default": { "aiofiles": { "hashes": [ - "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27", - "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092" + "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4", + "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc" ], - "version": "==0.6.0" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==0.7.0" + }, + "anyio": { + "hashes": [ + "sha256:41c4be842c284222b197a625d76a7ab85adf9d52788f563172fe180c2744b6c1", + "sha256:89e19b1498c8a6f12277e0bd2949597e445aa1b14361fbab2c36943639ef5190" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.2.0" }, "async-generator": { "hashes": [ @@ -33,11 +42,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "bech32": { "hashes": [ @@ -99,41 +108,40 @@ }, "cerberus": { "hashes": [ - "sha256:7aff49bc793e58a88ac14bffc3eca0f67e077881d3c62c621679a621294dd174", - "sha256:eec10585c33044fb7c69650bc5b68018dac0443753337e2b07684ee0f3c83329" + "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" ], "index": "pypi", - "version": "==1.3.3" + "version": "==1.3.4" }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2020.12.5" + "version": "==2021.5.30" }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" + "markers": "python_version >= '3.6'", + "version": "==8.0.1" }, "ecdsa": { "hashes": [ - "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747", - "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff" + "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676", + "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa" ], "index": "pypi", - "version": "==0.16.1" + "version": "==0.17.0" }, "embit": { "hashes": [ - "sha256:7c4264d7ede8e2c114db10585270874c9df809c68d2e21db918872e3245b5f2b" + "sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a" ], "index": "pypi", - "version": "==0.2.1" + "version": "==0.4.2" }, "environs": { "hashes": [ @@ -169,19 +177,19 @@ }, "httpcore": { "hashes": [ - "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9", - "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc" + "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", + "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff" ], "markers": "python_version >= '3.6'", - "version": "==0.12.3" + "version": "==0.13.6" }, "httpx": { "hashes": [ - "sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967", - "sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272" + "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c", + "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6" ], "index": "pypi", - "version": "==0.17.1" + "version": "==0.18.2" }, "hypercorn": { "extras": [ @@ -196,34 +204,34 @@ }, "hyperframe": { "hashes": [ - "sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1", - "sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34" + "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", + "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914" ], "markers": "python_full_version >= '3.6.1'", - "version": "==6.0.0" + "version": "==6.0.1" }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "version": "==3.1" + "version": "==3.2" }, "itsdangerous": { "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.0" + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.3" + "markers": "python_version >= '3.6'", + "version": "==3.0.1" }, "lnurl": { "hashes": [ @@ -235,69 +243,87 @@ }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.1.1" + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "marshmallow": { "hashes": [ - "sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd", - "sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b" + "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040", + "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01" ], "markers": "python_version >= '3.5'", - "version": "==3.11.1" + "version": "==3.12.1" + }, + "mypy": { + "hashes": [ + "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", + "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", + "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", + "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", + "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", + "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", + "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", + "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", + "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", + "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", + "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", + "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", + "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", + "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", + "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", + "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", + "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", + "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", + "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", + "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", + "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", + "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", + "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" + ], + "markers": "implementation_name == 'cpython'", + "version": "==0.902" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" }, "outcome": { "hashes": [ @@ -316,31 +342,31 @@ }, "pydantic": { "hashes": [ - "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850", - "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f", - "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683", - "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e", - "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3", - "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9", - "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c", - "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f", - "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a", - "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2", - "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125", - "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8", - "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99", - "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f", - "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0", - "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d", - "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520", - "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58", - "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771", - "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4", - "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e", - "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3" + "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", + "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", + "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", + "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", + "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", + "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", + "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", + "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", + "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", + "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", + "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", + "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", + "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", + "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", + "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", + "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", + "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", + "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", + "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", + "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", + "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", + "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" ], "markers": "python_full_version >= '3.6.1'", - "version": "==1.8.1" + "version": "==1.8.2" }, "pypng": { "hashes": [ @@ -366,18 +392,18 @@ }, "python-dotenv": { "hashes": [ - "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a", - "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2" + "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", + "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" ], - "version": "==0.17.0" + "version": "==0.18.0" }, "quart": { "hashes": [ - "sha256:429c5b4ff27e1d2f9ca0aacc38f6aba0ff49b38b815448bf24b613d3de12ea02", - "sha256:7b13786e07541cc9ce1466fdc6a6ccd5f36eb39118edd25a42d617593cd17707" + "sha256:f35134fb1d81af61624e6d89bca33cd611dcedce2dc4e291f527ab04395f4e1a", + "sha256:f80c91d1e0588662483e22dd9c368a5778886b62e128c5399d2cc1b1898482cf" ], "index": "pypi", - "version": "==0.14.1" + "version": "==0.15.1" }, "quart-compress": { "hashes": [ @@ -389,19 +415,19 @@ }, "quart-cors": { "hashes": [ - "sha256:0ea23ea8db2c21835f6698b91a09d99ab59f98f8d90a2a739475ef0409591573", - "sha256:e526e9929934ad31301853efe357a3bd2e08c3282aff37184fa8671ed854f052" + "sha256:c2be932f20413a56b176527090229afe8f725a3ee029d45ea08a174cdc319823", + "sha256:ea08d26aef918d59194fbf065cde9b6cae90dc5f21120dcd254d7d46190cd293" ], "index": "pypi", - "version": "==0.4.0" + "version": "==0.5.0" }, "quart-trio": { "hashes": [ - "sha256:1e7fce0df41afc3038bf0431b20614f90984de50341b19f9d4d3b9ba1ac7574a", - "sha256:933e3c18e232ece30ccbac7579fdc5f62f2f9c79c3273d6c341f5a1686791eb1" + "sha256:27617f0c9fa8759d3056e9ddcdc038d44093af45eb5f84f8d5714872aaaa8c7d", + "sha256:30dfab5e382f06c605d4a5960e8188e8e05d10198f02097f0a16c1dca41b3574" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "represent": { "hashes": [ @@ -416,18 +442,18 @@ "idna2008" ], "hashes": [ - "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", - "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", + "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "secure": { "hashes": [ - "sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447", - "sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd" + "sha256:6e30939d8f95bf3b8effb8a36ebb5ed57f265daeeae905e3aa9677ea538ab64e", + "sha256:a93b720c7614809c131ca80e477263140107c6c212829d0a6e1f7bc8d859c608" ], "index": "pypi", - "version": "==0.2.1" + "version": "==0.3.0" }, "shortuuid": { "hashes": [ @@ -439,11 +465,11 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" + "version": "==1.16.0" }, "sniffio": { "hashes": [ @@ -455,10 +481,10 @@ }, "sortedcontainers": { "hashes": [ - "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", - "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1" + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" ], - "version": "==2.3.0" + "version": "==2.4.0" }, "sqlalchemy": { "hashes": [ @@ -528,22 +554,30 @@ "index": "pypi", "version": "==0.16.0" }, - "typing-extensions": { + "trio-typing": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:35f1bec8df2150feab6c8b073b54135321722c9d9289bbffa78a9a091ea83b72", + "sha256:f2007df617a6c26a2294db0dd63645b5451149757e1bde4cb8dbf3e1369174fb" ], "index": "pypi", - "version": "==3.7.4.3" + "version": "==0.5.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "index": "pypi", + "version": "==3.10.0.0" }, "werkzeug": { "hashes": [ - "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", - "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" + "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", + "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.0.1" + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "wsproto": { "hashes": [ @@ -572,11 +606,11 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "black": { "hashes": [ @@ -587,11 +621,11 @@ }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" + "markers": "python_version >= '3.6'", + "version": "==8.0.1" }, "coverage": { "hashes": [ @@ -653,10 +687,10 @@ }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "version": "==3.1" + "version": "==3.2" }, "iniconfig": { "hashes": [ @@ -667,31 +701,32 @@ }, "mypy": { "hashes": [ - "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", - "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", - "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", - "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", - "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", - "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", - "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", - "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", - "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", - "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", - "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", - "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", - "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", - "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", - "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", - "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", - "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", - "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", - "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", - "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", - "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", - "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" + "sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2", + "sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4", + "sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8", + "sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da", + "sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243", + "sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb", + "sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116", + "sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0", + "sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76", + "sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20", + "sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c", + "sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1", + "sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab", + "sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269", + "sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2", + "sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4", + "sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70", + "sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9", + "sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd", + "sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987", + "sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21", + "sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167", + "sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8" ], - "index": "pypi", - "version": "==0.812" + "markers": "implementation_name == 'cpython'", + "version": "==0.902" }, "mypy-extensions": { "hashes": [ @@ -749,19 +784,19 @@ }, "pytest": { "hashes": [ - "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", - "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" ], "index": "pypi", - "version": "==6.2.3" + "version": "==6.2.4" }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", + "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.1" }, "pytest-trio": { "hashes": [ @@ -826,10 +861,10 @@ }, "sortedcontainers": { "hashes": [ - "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", - "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1" + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" ], - "version": "==2.3.0" + "version": "==2.4.0" }, "toml": { "hashes": [ @@ -884,12 +919,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "index": "pypi", - "version": "==3.7.4.3" + "version": "==3.10.0.0" } } } diff --git a/README.md b/README.md index 61fa79651..bc700bfd8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an * Fallback wallet for the LNURL scheme * Instant wallet for LN demonstrations -LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily. +LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly. See [lnbits.org](https://lnbits.org) for more detailed documentation. diff --git a/docs/devs/installation.md b/docs/devs/installation.md index 81ae2a4c6..013f7be90 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -5,15 +5,11 @@ title: Installation nav_order: 1 --- - -Installation -============ +# Installation Download the latest stable release https://github.com/lnbits/lnbits/releases - -Application dependencies ------------------------- +## Application dependencies The application uses [Pipenv][pipenv] to manage Python packages. While in development, you will need to install all dependencies: @@ -24,7 +20,7 @@ $ pipenv install --dev ``` If any of the modules fails to install, try checking and upgrading your setupTool module. -`pip install -U setuptools` +`pip install -U setuptools` If you wish to use a version of Python higher than 3.7: @@ -41,22 +37,33 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and ` Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. - -Running the server ------------------- +## Running the server LNbits uses [Quart][quart] as an application server. +Before running the server for the first time, make sure to create the data folder: + +```sh +$ mkdir data +``` + +To then run the server, use: ```sh $ pipenv run python -m lnbits ``` -Frontend --------- +**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run: + +```sh +$ ./nrok http 5000 +``` + +this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits. + +## Frontend The frontend uses [Vue.js and Quasar][quasar]. - [quart]: https://pgjones.gitlab.io/ [pipenv]: https://pipenv.pypa.io/ [polar]: https://lightningpolar.com/ diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 4f1bb8530..9fb8a3e32 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -13,18 +13,48 @@ Download this repo and install the dependencies: ```sh git clone https://github.com/lnbits/lnbits.git cd lnbits/ +# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work python3 -m venv venv ./venv/bin/pip install -r requirements.txt cp .env.example .env +mkdir data ./venv/bin/quart assets ./venv/bin/quart migrate ./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' ``` -No you can visit your LNbits at http://localhost:5000/. +Now you can visit your LNbits at http://localhost:5000/. Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source. -Then you can run restart it and it will be using the new settings. +Then you can restart it and it will be using the new settings. You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source. + +Docker installation +=================== + +To install using docker you first need to build the docker image as: +``` +git clone https://github.com/lnbits/lnbits.git +cd lnbits/ # ${PWD} referred as +docker build -t lnbits . +``` + +You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there +``` +cp /.env.example .env +``` +and change the configuration in `.env` as required. + +Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container. +``` +mkdir data +sudo chown 1000:1000 ./data/ +``` + +Then the image can be run as: +``` +docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits +``` +Finally you can access your lnbits on your machine at port 5000. diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 64b61bae9..888fc8d6c 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -54,7 +54,7 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho ### LNPay -For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `/wallet/webhook` with the "Wallet Receive" event and no secret. +For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment. - `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet** - `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/ diff --git a/lnbits/__main__.py b/lnbits/__main__.py index fa75231c4..90b086423 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio from .commands import migrate_databases, transpile_scss, bundle_vendored diff --git a/lnbits/app.py b/lnbits/app.py index cd700f5c9..35852cd9b 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,6 +1,7 @@ import sys -import importlib import warnings +import importlib +import traceback from quart import g from quart_trio import QuartTrio @@ -23,7 +24,6 @@ from .tasks import ( invoice_listener, internal_invoice_listener, webhook_handler, - grab_app_for_later, ) from .settings import WALLET @@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio: register_commands(app) register_request_hooks(app) register_async_tasks(app) - grab_app_for_later(app) + register_exception_handlers(app) return app @@ -108,16 +108,13 @@ def register_assets(app: QuartTrio): def register_filters(app: QuartTrio): """Jinja filters.""" app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] + app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"] app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions() def register_request_hooks(app: QuartTrio): """Open the core db for each request so everything happens in a big transaction""" - @app.before_request - async def before_request(): - g.nursery = app.nursery - @app.after_request async def set_secure_headers(response): secure_headers.quart(response) @@ -139,3 +136,21 @@ def register_async_tasks(app): @app.after_serving async def stop_listeners(): pass + + +def register_exception_handlers(app): + @app.errorhandler(Exception) + async def basic_error(err): + etype, value, tb = sys.exc_info() + traceback.print_exception(etype, err, tb) + exc = traceback.format_exc() + return ( + "\n\n".join( + [ + "LNbits internal error!", + exc, + "If you believe this shouldn't be an error please bring it up on https://t.me/lnbits", + ] + ), + 500, + ) diff --git a/lnbits/commands.py b/lnbits/commands.py index 2be04d127..2e9b837fd 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio import warnings import click import importlib diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index ca0959a89..12dcded80 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -14,6 +14,7 @@ core_app: Blueprint = Blueprint( from .views.api import * # noqa from .views.generic import * # noqa +from .views.public_api import * # noqa from .tasks import register_listeners from lnbits.tasks import record_async diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index b9f02070f..47623cc26 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -267,12 +267,23 @@ async def get_payments( async def delete_expired_invoices( conn: Optional[Connection] = None, ) -> None: + # first we delete all invoices older than one month + await (conn or db).execute( + """ + DELETE FROM apipayments + WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 2592000 + """ + ) + + # then we delete all expired invoices, checking one by one rows = await (conn or db).fetchall( """ SELECT bolt11 FROM apipayments - WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400 - """ + WHERE pending = 1 + AND bolt11 IS NOT NULL + AND amount > 0 AND time < strftime('%s', 'now') - 86400 + """ ) for (payment_request,) in rows: try: diff --git a/lnbits/core/services.py b/lnbits/core/services.py index f2821fdcb..09b9f4f71 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio import json import httpx from io import BytesIO @@ -229,10 +229,10 @@ async def redeem_lnurl_withdraw( pass async with httpx.AsyncClient() as client: - await client.get( - res["callback"], - params=params, - ) + try: + await client.get(res["callback"], params=params) + except Exception: + pass async def perform_lnurlauth( diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 7e91278bd..d01910514 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -119,6 +119,8 @@ new Vue({ paymentHash: null, minMax: [0, 2100000000000000], lnurl: null, + units: ['sat'], + unit: 'sat', data: { amount: null, memo: '' @@ -233,6 +235,7 @@ new Vue({ this.receive.paymentHash = null this.receive.data.amount = null this.receive.data.memo = null + this.receive.unit = 'sat' this.receive.paymentChecker = null this.receive.minMax = [0, 2100000000000000] this.receive.lnurl = null @@ -269,11 +272,13 @@ new Vue({ }, createInvoice: function () { this.receive.status = 'loading' + LNbits.api .createInvoice( this.g.wallet, this.receive.data.amount, this.receive.data.memo, + this.receive.unit, this.receive.lnurl && this.receive.lnurl.callback ) .then(response => { @@ -619,6 +624,15 @@ new Vue({ created: function () { this.fetchBalance() this.fetchPayments() + + LNbits.api + .request('GET', '/api/v1/currencies') + .then(response => { + this.receive.units = ['sat', ...response.data] + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) }, mounted: function () { // show disclaimer diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 20740c05d..fa2df9645 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio import httpx from typing import List @@ -8,7 +8,7 @@ from . import db from .crud import get_balance_notify from .models import Payment -sse_listeners: List[trio.MemorySendChannel] = [] +api_invoice_listeners: List[trio.MemorySendChannel] = [] async def register_listeners(): @@ -20,7 +20,7 @@ async def register_listeners(): async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async for payment in invoice_paid_chan: # send information to sse channel - await dispatch_sse(payment) + await dispatch_invoice_listener(payment) # dispatch webhook if payment.webhook and not payment.webhook_status: @@ -40,13 +40,13 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): pass -async def dispatch_sse(payment: Payment): - for send_channel in sse_listeners: +async def dispatch_invoice_listener(payment: Payment): + for send_channel in api_invoice_listeners: try: send_channel.send_nowait(payment) except trio.WouldBlock: print("removing sse listener", send_channel) - sse_listeners.remove(send_channel) + api_invoice_listeners.remove(send_channel) async def dispatch_webhook(payment: Payment): diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index c68517936..d127b6f9e 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -129,7 +129,7 @@ #{{ props.row.tag }} @@ -313,12 +313,21 @@ {{receive.lnurl.domain}} is requesting an invoice:

+ ", methods=["GET"]) +async def api_public_payment_longpolling(payment_hash): + payment = await get_standalone_payment(payment_hash) + + if not payment: + return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND + elif not payment.pending: + return jsonify({"status": "paid"}), HTTPStatus.OK + + try: + invoice = bolt11.decode(payment.bolt11) + expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry) + if expiration < datetime.datetime.now(): + return jsonify({"status": "expired"}), HTTPStatus.OK + except: + return jsonify({"message": "Invalid bolt11 invoice."}), HTTPStatus.BAD_REQUEST + + send_payment, receive_payment = trio.open_memory_channel(0) + + print("adding standalone invoice listener", payment_hash, send_payment) + api_invoice_listeners.append(send_payment) + + async for payment in receive_payment: + if payment.payment_hash == payment_hash: + return jsonify({"status": "paid"}), HTTPStatus.OK diff --git a/lnbits/db.py b/lnbits/db.py index 82bda2141..9da3b6ecf 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -3,7 +3,7 @@ import trio from contextlib import asynccontextmanager from sqlalchemy import create_engine # type: ignore from sqlalchemy_aio import TRIO_STRATEGY # type: ignore -from sqlalchemy_aio.base import AsyncConnection +from sqlalchemy_aio.base import AsyncConnection # type: ignore from .settings import LNBITS_DATA_FOLDER diff --git a/lnbits/extensions/events/README.md b/lnbits/extensions/events/README.md index 9108519c8..11b62fecb 100644 --- a/lnbits/extensions/events/README.md +++ b/lnbits/extensions/events/README.md @@ -1,3 +1,33 @@ -

Events

-

Events: Sell and register event tickets

-Events alows you to make a wave of tickets for an event, each ticket is in the form of a unqiue QRcode, which the user presents at registration. Events comes with a shareable ticket scanner, which can be used to register attendees. +# Events + +## Sell tickets for events and use the built-in scanner for registering attendants + +Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance. + +Events includes a shareable ticket scanner, which can be used to register attendees. + +## Usage + +1. Create an event\ + ![create event](https://i.imgur.com/dadK1dp.jpg) +2. Fill out the event information: + + - event name + - wallet (normally there's only one) + - event information + - closing date for event registration + - begin and end date of the event + + ![event info](https://imgur.com/KAv68Yr.jpg) + +3. Share the event registration link\ + ![event ticket](https://imgur.com/AQWUOBY.jpg) + + - ticket example\ + ![ticket example](https://i.imgur.com/trAVSLd.jpg) + + - QR code ticket, presented after invoice paid, to present at registration\ + ![event ticket](https://i.imgur.com/M0ROM82.jpg) + +4. Use the built-in ticket scanner to validate registered, and paid, attendees\ + ![ticket scanner](https://i.imgur.com/zrm9202.jpg) diff --git a/lnbits/extensions/events/templates/events/register.html b/lnbits/extensions/events/templates/events/register.html index b1a6a6c18..63af65e85 100644 --- a/lnbits/extensions/events/templates/events/register.html +++ b/lnbits/extensions/events/templates/events/register.html @@ -82,7 +82,7 @@ + + {% endblock %} + diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html new file mode 100644 index 000000000..25cc49e52 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -0,0 +1,189 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + Add Spotify Jukebox + + {% raw %} + + + + + + + {% endraw %} + + +
+ +
+ + +
LNbits jukebox extension
+
+ + + {% include "jukebox/_api_docs.html" %} + +
+
+ + + + + + + + + +
+
+ Continue + Continue +
+
+ Cancel +
+
+ +
+
+ + + + To use this extension you need a Spotify client ID and client secret. + You get these by creating an app in the Spotify developers dashboard + here. + + + + + + + +
+
+ Submit keys + Submit keys +
+
+ Cancel +
+
+ +
+
+ + + + In the app go to edit-settings, set the redirect URI to this link +
+ {% raw %}{{ locationcb + }}{{ jukeboxDialog.data.sp_id }}{% endraw + %} Click to copy URL + +
+ Settings can be found + here. + +
+
+ Authorise access + Authorise access +
+
+ Cancel +
+
+ +
+
+ + + + +
+
+ Create Jukebox + Create Jukebox +
+
+ Cancel +
+
+
+
+
+
+ + + +
+
Shareable Jukebox QR
+
+ + + +
+ + Copy jukebox link + Open jukebox + Close +
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html new file mode 100644 index 000000000..7eeb5de35 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -0,0 +1,276 @@ +{% extends "public.html" %} {% block page %} {% raw %} +
+
+ + +

Currently playing

+
+
+ +
+
+ {{ currentPlay.name }}
+ {{ currentPlay.artist }} +
+
+
+
+ + + +

Pick a song

+ + +
+ + + + + + +
+
+ + + + +
+
+ +
+
+ {{ receive.name }}
+ {{ receive.artist }} +
+
+
+
+
+ Play for {% endraw %}{{ price }}{% raw %} sats + +
+
+
+ + + + + +
+ Copy invoice +
+
+
+
+{% endraw %} {% endblock %} {% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py new file mode 100644 index 000000000..f439110a0 --- /dev/null +++ b/lnbits/extensions/jukebox/views.py @@ -0,0 +1,42 @@ +import time +from datetime import datetime +from quart import g, render_template, request, jsonify, websocket +from http import HTTPStatus +import trio +from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.core.models import Payment + +import json +from . import jukebox_ext +from .crud import get_jukebox +from .views_api import api_get_jukebox_device_check + + +@jukebox_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("jukebox/index.html", user=g.user) + + +@jukebox_ext.route("/") +async def connect_to_jukebox(juke_id): + jukebox = await get_jukebox(juke_id) + if not jukebox: + return "error" + deviceCheck = await api_get_jukebox_device_check(juke_id) + devices = json.loads(deviceCheck[0].text) + deviceConnected = False + for device in devices["devices"]: + if device["id"] == jukebox.sp_device.split("-")[1]: + deviceConnected = True + if deviceConnected: + return await render_template( + "jukebox/jukebox.html", + playlists=jukebox.sp_playlists.split(","), + juke_id=juke_id, + price=jukebox.price, + inkey=jukebox.inkey, + ) + else: + return await render_template("jukebox/error.html") diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py new file mode 100644 index 000000000..4bd1fb40f --- /dev/null +++ b/lnbits/extensions/jukebox/views_api.py @@ -0,0 +1,490 @@ +from quart import g, jsonify, request +from http import HTTPStatus +import base64 +from lnbits.core.crud import get_wallet +from lnbits.core.services import create_invoice, check_invoice_status +import json + +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +import httpx +from . import jukebox_ext +from .crud import ( + create_jukebox, + update_jukebox, + get_jukebox, + get_jukeboxs, + delete_jukebox, + create_jukebox_payment, + get_jukebox_payment, + update_jukebox_payment, +) +from lnbits.core.services import create_invoice, check_invoice_status + + +@jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_get_jukeboxs(): + try: + return ( + jsonify( + [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "", HTTPStatus.NO_CONTENT + + +##################SPOTIFY AUTH##################### + + +@jukebox_ext.route("/api/v1/jukebox/spotify/cb/", methods=["GET"]) +async def api_check_credentials_callbac(juke_id): + sp_code = "" + sp_access_token = "" + sp_refresh_token = "" + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + if request.args.get("code"): + sp_code = request.args.get("code") + jukebox = await update_jukebox( + juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code + ) + if request.args.get("access_token"): + sp_access_token = request.args.get("access_token") + sp_refresh_token = request.args.get("refresh_token") + jukebox = await update_jukebox( + juke_id=juke_id, + sp_secret=jukebox.sp_secret, + sp_access_token=sp_access_token, + sp_refresh_token=sp_refresh_token, + ) + return "

Success!

You can close this window

" + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_check_credentials_check(juke_id): + jukebox = await get_jukebox(juke_id) + return jsonify(jukebox._asdict()), HTTPStatus.CREATED + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) +@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "user": {"type": "string", "empty": False, "required": True}, + "title": {"type": "string", "empty": False, "required": True}, + "wallet": {"type": "string", "empty": False, "required": True}, + "sp_user": {"type": "string", "empty": False, "required": True}, + "sp_secret": {"type": "string", "required": True}, + "sp_access_token": {"type": "string", "required": False}, + "sp_refresh_token": {"type": "string", "required": False}, + "sp_device": {"type": "string", "required": False}, + "sp_playlists": {"type": "string", "required": False}, + "price": {"type": "string", "required": False}, + } +) +async def api_create_update_jukebox(juke_id=None): + if juke_id: + jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **g.data) + else: + jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data) + + return jsonify(jukebox._asdict()), HTTPStatus.CREATED + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_delete_item(juke_id): + await delete_jukebox(juke_id) + try: + return ( + jsonify( + [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "", HTTPStatus.NO_CONTENT + + +################JUKEBOX ENDPOINTS################## + +######GET ACCESS TOKEN###### + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/playlist//", methods=["GET"] +) +async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + tracks = [] + async with httpx.AsyncClient() as client: + try: + r = await client.get( + "https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if "items" not in r.json(): + if r.status_code == 401: + token = await api_get_token(juke_id) + if token == False: + return False + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_song( + juke_id, sp_playlist, retry=True + ) + return r, HTTPStatus.OK + for item in r.json()["items"]: + tracks.append( + { + "id": item["track"]["id"], + "name": item["track"]["name"], + "album": item["track"]["album"]["name"], + "artist": item["track"]["artists"][0]["name"], + "image": item["track"]["album"]["images"][0]["url"], + } + ) + except AssertionError: + something = None + return jsonify([track for track in tracks]) + + +async def api_get_token(juke_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + + async with httpx.AsyncClient() as client: + try: + r = await client.post( + "https://accounts.spotify.com/api/token", + timeout=40, + params={ + "grant_type": "refresh_token", + "refresh_token": jukebox.sp_refresh_token, + "client_id": jukebox.sp_user, + }, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic " + + base64.b64encode( + str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii") + ).decode("ascii"), + "Content-Type": "application/x-www-form-urlencoded", + }, + ) + if "access_token" not in r.json(): + return False + else: + await update_jukebox( + juke_id=juke_id, sp_access_token=r.json()["access_token"] + ) + except AssertionError: + something = None + return True + + +######CHECK DEVICE + + +@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) +async def api_get_jukebox_device_check(juke_id, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + async with httpx.AsyncClient() as client: + rDevice = await client.get( + "https://api.spotify.com/v1/me/player/devices", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + + if rDevice.status_code == 204 or rDevice.status_code == 200: + return ( + rDevice, + HTTPStatus.OK, + ) + elif rDevice.status_code == 401 or rDevice.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return api_get_jukebox_device_check(juke_id, retry=True) + else: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.FORBIDDEN, + ) + + +######GET INVOICE STUFF + + +@jukebox_ext.route("/api/v1/jukebox/jb/invoice//", methods=["GET"]) +async def api_get_jukebox_invoice(juke_id, song_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + try: + deviceCheck = await api_get_jukebox_device_check(juke_id) + devices = json.loads(deviceCheck[0].text) + deviceConnected = False + for device in devices["devices"]: + if device["id"] == jukebox.sp_device.split("-")[1]: + deviceConnected = True + if not deviceConnected: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.NOT_FOUND, + ) + except: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.NOT_FOUND, + ) + + invoice = await create_invoice( + wallet_id=jukebox.wallet, + amount=jukebox.price, + memo=jukebox.title, + extra={"tag": "jukebox"}, + ) + + jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id) + + return jsonify(invoice, jukebox_payment) + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/checkinvoice//", methods=["GET"] +) +async def api_get_jukebox_invoice_check(pay_hash, juke_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + try: + status = await check_invoice_status(jukebox.wallet, pay_hash) + is_paid = not status.pending + except Exception as exc: + return jsonify({"paid": False}), HTTPStatus.OK + if is_paid: + wallet = await get_wallet(jukebox.wallet) + payment = await wallet.get_payment(pay_hash) + await payment.set_pending(False) + await update_jukebox_payment(pay_hash, paid=True) + return jsonify({"paid": True}), HTTPStatus.OK + return jsonify({"paid": False}), HTTPStatus.OK + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/invoicep///", methods=["GET"] +) +async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + jukebox_payment = await get_jukebox_payment(pay_hash) + if jukebox_payment.paid: + async with httpx.AsyncClient() as client: + r = await client.get( + "https://api.spotify.com/v1/me/player/currently-playing?market=ES", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + rDevice = await client.get( + "https://api.spotify.com/v1/me/player", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + isPlaying = False + if rDevice.status_code == 200: + isPlaying = rDevice.json()["is_playing"] + + if r.status_code == 204 or isPlaying == False: + async with httpx.AsyncClient() as client: + uri = ["spotify:track:" + song_id] + r = await client.put( + "https://api.spotify.com/v1/me/player/play?device_id=" + + jukebox.sp_device.split("-")[1], + json={"uris": uri}, + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify(jukebox_payment), HTTPStatus.OK + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash, retry=True + ) + else: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif r.status_code == 200: + async with httpx.AsyncClient() as client: + r = await client.post( + "https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A" + + song_id + + "&device_id=" + + jukebox.sp_device.split("-")[1], + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify(jukebox_payment), HTTPStatus.OK + + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash + ) + else: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash + ) + return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK + + +############################GET TRACKS + + +@jukebox_ext.route("/api/v1/jukebox/jb/currently/", methods=["GET"]) +async def api_get_jukebox_currently(juke_id, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + async with httpx.AsyncClient() as client: + try: + r = await client.get( + "https://api.spotify.com/v1/me/player/currently-playing?market=ES", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify({"error": "Nothing"}), HTTPStatus.OK + elif r.status_code == 200: + try: + response = r.json() + + track = { + "id": response["item"]["id"], + "name": response["item"]["name"], + "album": response["item"]["album"]["name"], + "artist": response["item"]["artists"][0]["name"], + "image": response["item"]["album"]["images"][0]["url"], + } + return jsonify(track), HTTPStatus.OK + except: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + + elif r.status_code == 401: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_currently(juke_id, retry=True) + else: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + except AssertionError: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND diff --git a/lnbits/extensions/livestream/README.md b/lnbits/extensions/livestream/README.md index 59e54000a..4e88e7bc7 100644 --- a/lnbits/extensions/livestream/README.md +++ b/lnbits/extensions/livestream/README.md @@ -1,13 +1,45 @@ # DJ Livestream -An extension to help DJs to conduct music livestreams. +## Help DJ's and music producers conduct music livestreams -It produces a single static QR code that can be shown on screen. Once someone scans that QR code with an lnurl-pay capable wallet they will see the name of the track being played at that time and the name of the producer. +LNBits Livestream extension produces a static QR code that can be shown on screen while livestreaming a DJ set for example. If someone listening to the livestream likes a song and want to support the DJ and/or the producer he can scan the QR code with a LNURL-pay capable wallet. -They will then be given the opportunity to send a tip and a message related to that specific track and if they pay an amount over a specific threshold they will be given a link to download it (optional). +When scanned, the QR code sends up information about the song playing at the moment (name and the producer of that song). Also, if the user likes the song and would like to support the producer, he can send a tip and a message for that specific track. If the user sends an amount over a specific threshold they will be given a link to download it (optional). The revenue will be sent to a wallet created specifically for that producer, with optional revenue splitting between the DJ and the producer. +[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) + +[![video tutorial livestream](http://img.youtube.com/vi/zDrSWShKz7k/0.jpg)](https://youtu.be/zDrSWShKz7k 'video tutorial offline shop') + +## Usage + +1. Start by adding a track\ + ![add new track](https://i.imgur.com/Cu0eGrW.jpg) + - set the producer, or choose an existing one + - set the track name + - define a minimum price where a user can download the track + - set the download URL, where user will be redirected if he tips the livestream and the tip is equal or above the set price\ + ![track settings](https://i.imgur.com/HTJYwcW.jpg) +2. Adjust the percentage of the pay you want to take from the user's tips. 10%, the default, means that the DJ will keep 10% of all the tips sent by users. The other 90% will go to an auto generated producer wallet\ + ![adjust percentage](https://i.imgur.com/9weHKAB.jpg) +3. For every different producer added, when adding tracks, a wallet is generated for them\ + ![producer wallet](https://i.imgur.com/YFIZ7Tm.jpg) +4. On the bottom of the LNBits DJ Livestream extension you'll find the static QR code ([LNURL-pay](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) you can add to the livestream or if you're a street performer you can print it and have it displayed +5. After all tracks and producers are added, you can start "playing" songs\ + ![play tracks](https://i.imgur.com/7ytiBkq.jpg) +6. You'll see the current track playing and a green icon indicating active track also\ + ![active track](https://i.imgur.com/W1vBz54.jpg) +7. When a user scans the QR code, and sends a tip, you'll receive 10%, in the example case, in your wallet and the producer's wallet will get the rest. For example someone tips 100 sats, you'll get 10 sats and the producer will get 90 sats + - producer's wallet receiving 18 sats from 20 sats tips\ + ![producer wallet](https://i.imgur.com/OM9LawA.jpg) + +## Use cases + +You can print the QR code and display it on a live gig, a street performance, etc... OR you can use the QR as an overlay in an online stream of you playing music, doing a DJ set, making a podcast. + +You can use the extension's API to trigger updates for the current track, update fees, add tracks... + ## Sponsored by [![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/) diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py index 1e021f85b..3b9e7e316 100644 --- a/lnbits/extensions/livestream/lnurl.py +++ b/lnbits/extensions/livestream/lnurl.py @@ -61,7 +61,7 @@ async def lnurl_callback(track_id): if not track: return jsonify({"status": "ERROR", "reason": "Couldn't find track."}) - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < track.min_sendable: return ( diff --git a/lnbits/extensions/livestream/static/js/index.js b/lnbits/extensions/livestream/static/js/index.js index faa439cb9..c49befce2 100644 --- a/lnbits/extensions/livestream/static/js/index.js +++ b/lnbits/extensions/livestream/static/js/index.js @@ -37,6 +37,7 @@ new Vue({ }, methods: { getTrackLabel(trackId) { + if (!trackId) return let track = this.tracksMap[trackId] return `${track.name}, ${this.producersMap[track.producer].name}` }, @@ -162,6 +163,7 @@ new Vue({ }) }, updateCurrentTrack(track) { + console.log(this.nextCurrentTrack, this.livestream) if (this.livestream.current_track === track) { // if clicking the same, stop it track = 0 @@ -175,6 +177,7 @@ new Vue({ ) .then(() => { this.livestream.current_track = track + this.nextCurrentTrack = track this.$q.notify({ message: `Current track updated.`, timeout: 700 diff --git a/lnbits/extensions/livestream/tasks.py b/lnbits/extensions/livestream/tasks.py index c69db02ad..52f86d155 100644 --- a/lnbits/extensions/livestream/tasks.py +++ b/lnbits/extensions/livestream/tasks.py @@ -1,5 +1,5 @@ import json -import trio # type: ignore +import trio from lnbits.core.models import Payment from lnbits.core.crud import create_payment diff --git a/lnbits/extensions/livestream/templates/livestream/index.html b/lnbits/extensions/livestream/templates/livestream/index.html index 40ce19cdc..e6585ac04 100644 --- a/lnbits/extensions/livestream/templates/livestream/index.html +++ b/lnbits/extensions/livestream/templates/livestream/index.html @@ -27,8 +27,8 @@
{% raw %} - {{ nextCurrentTrack === livestream.current_track ? 'Stop' : 'Set' - }} current track + {{ nextCurrentTrack && nextCurrentTrack === + livestream.current_track ? 'Stop' : 'Set' }} current track {% endraw %}
diff --git a/lnbits/extensions/lnticket/README.md b/lnbits/extensions/lnticket/README.md index b251f44bd..bd0714506 100644 --- a/lnbits/extensions/lnticket/README.md +++ b/lnbits/extensions/lnticket/README.md @@ -1,3 +1,29 @@ -

Support Tickets

-

Get paid sats to answer questions

-Charge people per word for contacting you. Possible applications include, paid support ticketing, PAYG language services, contact spam protection. +# Support Tickets + +## Get paid sats to answer questions + +Charge a per word amount for people to contact you. + +Possible applications include, paid support ticketing, PAYG language services, contact spam protection. + +1. Click "NEW FORM" to create a new contact form\ + ![new contact form](https://i.imgur.com/kZqWGPe.png) +2. Fill out the contact form + - set the wallet to use + - give your form a name + - set an optional webhook that will get called when the form receives a payment + - give it a small description + - set the amount you want to charge, per **word**, for people to contact you\ + ![form settings](https://i.imgur.com/AsXeVet.png) +3. Your new contact form will appear on the _Forms_ section. Note that you can create various forms with different rates per word, for different purposes\ + ![forms section](https://i.imgur.com/gg71HhM.png) +4. When a user wants to reach out to you, they will get to the contact form. They can fill out some information: + - a name + - an optional email if they want you to reply + - and the actual message + - at the bottom, a value in satoshis, will display how much it will cost them to send this message\ + ![user view of form](https://i.imgur.com/DWGJWQz.png) + - after submiting the Lightning Network invoice will pop up and after payment the message will be sent to you\ + ![contact form payment](https://i.imgur.com/7heGsiO.png) +5. Back in "Support ticket" extension you'll get the messages your fans, users, haters, etc, sent you on the _Tickets_ section\ + ![tickets](https://i.imgur.com/dGhJ6Ok.png) diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index 0e0aa146a..cfdadc402 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -10,3 +10,8 @@ lnticket_ext: Blueprint = Blueprint( from .views_api import * # noqa from .views import * # noqa +from .tasks import register_listeners + +from lnbits.tasks import record_async + +lnticket_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py new file mode 100644 index 000000000..41395389a --- /dev/null +++ b/lnbits/extensions/lnticket/tasks.py @@ -0,0 +1,36 @@ +import json +import trio # type: ignore + +from lnbits.core.models import Payment +from lnbits.core.crud import create_payment +from lnbits.core import db as core_db +from lnbits.tasks import register_invoice_listener, internal_invoice_paid +from lnbits.helpers import urlsafe_short_hash + +from .crud import get_ticket, set_ticket_paid + + +async def register_listeners(): + invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) + register_invoice_listener(invoice_paid_chan_send) + await wait_for_paid_invoices(invoice_paid_chan_recv) + + +async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): + async for payment in invoice_paid_chan: + await on_invoice_paid(payment) + +async def on_invoice_paid(payment: Payment) -> None: + if "lnticket" != payment.extra.get("tag"): + # not a lnticket invoice + return + + ticket = await get_ticket(payment.checking_id) + if not ticket: + print("this should never happen", payment) + return + + await payment.set_pending(False) + await set_ticket_paid(payment.payment_hash) + _ticket = await get_ticket(payment.checking_id) + print('ticket', _ticket) diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html index 9a5accaf2..b432ce9ef 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/display.html +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -77,7 +77,7 @@ {% endblock %} {% block scripts %} diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html index 848623076..d2ab78018 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/index.html +++ b/lnbits/extensions/lnticket/templates/lnticket/index.html @@ -90,6 +90,16 @@
Tickets
+
Export to CSV {% endblock %} {% block scripts %} {{ window_vars(user) }} +{% endblock %} diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py new file mode 100644 index 000000000..acded737e --- /dev/null +++ b/lnbits/extensions/splitpayments/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import splitpayments_ext + + +@splitpayments_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("splitpayments/index.html", user=g.user) diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py new file mode 100644 index 000000000..e0fe475ed --- /dev/null +++ b/lnbits/extensions/splitpayments/views_api.py @@ -0,0 +1,70 @@ +from quart import g, jsonify +from http import HTTPStatus + +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.core.crud import get_wallet, get_wallet_for_key + +from . import splitpayments_ext +from .crud import get_targets, set_targets +from .models import Target + + +@splitpayments_ext.route("/api/v1/targets", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_targets_get(): + targets = await get_targets(g.wallet.id) + return jsonify([target._asdict() for target in targets] or []) + + +@splitpayments_ext.route("/api/v1/targets", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "targets": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "wallet": {"type": "string"}, + "alias": {"type": "string"}, + "percent": {"type": "integer"}, + }, + }, + } + } +) +async def api_targets_set(): + targets = [] + + for entry in g.data["targets"]: + wallet = await get_wallet(entry["wallet"]) + if not wallet: + wallet = await get_wallet_for_key(entry["wallet"], "invoice") + if not wallet: + return ( + jsonify({"message": f"Invalid wallet '{entry['wallet']}'."}), + HTTPStatus.BAD_REQUEST, + ) + + if wallet.id == g.wallet.id: + return ( + jsonify({"message": "Can't split to itself."}), + HTTPStatus.BAD_REQUEST, + ) + + if entry["percent"] < 0: + return ( + jsonify({"message": f"Invalid percent '{entry['percent']}'."}), + HTTPStatus.BAD_REQUEST, + ) + + targets.append( + Target(wallet.id, g.wallet.id, entry["percent"], entry["alias"] or "") + ) + + percent_sum = sum([target.percent for target in targets]) + if percent_sum > 100: + return jsonify({"message": "Splitting over 100%."}), HTTPStatus.BAD_REQUEST + + await set_targets(g.wallet.id, targets) + return "", HTTPStatus.OK diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md index 49dfc223d..729f40f41 100644 --- a/lnbits/extensions/subdomains/README.md +++ b/lnbits/extensions/subdomains/README.md @@ -1,27 +1,29 @@

Subdomains Extension

-So the goal of the extension is to allow the owner of a domain to sell their subdomain to the anyone who is willing to pay some money for it. +So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it. + +[![video tutorial livestream](http://img.youtube.com/vi/O1X0fy3uNpw/0.jpg)](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains') ## Requirements -- Free cloudflare account -- Cloudflare as a dns server provider -- Cloudflare TOKEN and Cloudflare zone-id where the domain is parked +- Free Cloudflare account +- Cloudflare as a DNS server provider +- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked ## Usage -1. Register at cloudflare and setup your domain with them. (Just follow instructions they provide...) -2. Change DNS server at your domain registrar to point to cloudflare's -3. Get Cloudflare zoneID for your domain +1. Register at Cloudflare and setup your domain with them. (Just follow instructions they provide...) +2. Change DNS server at your domain registrar to point to Cloudflare's +3. Get Cloudflare zone-ID for your domain -4. get Cloudflare API TOKEN +4. Get Cloudflare API TOKEN -5. Open the lnbits subdomains extension and register your domain with lnbits +5. Open the LNBits subdomains extension and register your domain 6. Click on the button in the table to open the public form that was generated for your domain -- Extension also supports webhooks so you can get notified when someone buys a new domain - + - Extension also supports webhooks so you can get notified when someone buys a new subdomain\ + ## API Endpoints @@ -36,8 +38,6 @@ So the goal of the extension is to allow the owner of a domain to sell their sub - GET /api/v1/subdomains/ - DELETE /api/v1/subdomains/ -## Useful - ### Cloudflare - Cloudflare offers programmatic subdomain registration... (create new A record) diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py index 09c3f73d7..b15703fb6 100644 --- a/lnbits/extensions/subdomains/tasks.py +++ b/lnbits/extensions/subdomains/tasks.py @@ -1,6 +1,6 @@ from http import HTTPStatus from quart.json import jsonify -import trio # type: ignore +import trio import httpx from .crud import get_domain, set_subdomain_paid diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index d62f8f385..74fd4ade6 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -452,7 +452,10 @@ id: this.domainDialog.data.wallet }) var data = this.domainDialog.data - data.allowed_record_types = data.allowed_record_types.join(', ') + data.allowed_record_types = + typeof data.allowed_record_types === 'string' + ? data.allowed_record_types + : data.allowed_record_types.join(', ') console.log(this.domainDialog) if (data.id) { this.updateDomain(wallet, data) diff --git a/lnbits/extensions/tpos/README.md b/lnbits/extensions/tpos/README.md index dd4491ecc..04e049e37 100644 --- a/lnbits/extensions/tpos/README.md +++ b/lnbits/extensions/tpos/README.md @@ -1,7 +1,15 @@ -

TPOS

-

A Shareable PoS that doesnt need to be installed and can run in phones browser!

-

If someone drops your Quickening in a beer at your bar, staff can scan a QR and open/use a browser based PoS, linked to the same self-hosted lnbits instance!

+# TPoS - +## A Shareable PoS (Point of Sale) that doesn't need to be installed and can run in the browser! +An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your business. The PoS is isolated from the wallet, so it's safe for any employee to use. You can create as many TPOS's as you need, for example one for each employee, or one for each branch of your business. +### Usage + +1. Enable extension +2. Create a TPOS\ + ![create](https://imgur.com/8jNj8Zq.jpg) +3. Open TPOS on the browser\ + ![open](https://imgur.com/LZuoWzb.jpg) +4. Present invoice QR to costumer\ + ![pay](https://imgur.com/tOwxn77.jpg) diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md index 6cb9deca9..b6f306275 100644 --- a/lnbits/extensions/usermanager/README.md +++ b/lnbits/extensions/usermanager/README.md @@ -1,3 +1,26 @@ -

User Manager

-

Make and manager users/wallets

-To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the develpoers stack as the user and wallet manager. +# User Manager + +## Make and manage users/wallets + +To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. + +For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users. + +## Usage + +1. Click the button "NEW USER" to create a new user\ + ![new user](https://i.imgur.com/4yZyfJE.png) +2. Fill the user information\ + - username + - the generated wallet name, user can create other wallets later on + - email + - set a password + ![user information](https://i.imgur.com/40du7W5.png) +3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section. +4. Next you can share the wallet with the corresponding user\ + ![user wallet](https://i.imgur.com/gAyajbx.png) +5. If you need to create more wallets for some user, click "NEW WALLET" at the top\ + ![multiple wallets](https://i.imgur.com/wovVnim.png) + - select the existing user you wish to add the wallet + - set a wallet name\ + ![new wallet](https://i.imgur.com/sGwG8dC.png) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index 6470c413a..dbee287c9 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -17,7 +17,11 @@ from .models import Users, Wallets async def create_usermanager_user( - user_name: str, wallet_name: str, admin_id: str + user_name: str, + wallet_name: str, + admin_id: str, + email: Optional[str] = None, + password: Optional[str] = None, ) -> Users: account = await create_account() user = await get_user(account.id) @@ -27,10 +31,10 @@ async def create_usermanager_user( await db.execute( """ - INSERT INTO users (id, name, admin) - VALUES (?, ?, ?) + INSERT INTO users (id, name, admin, email, password) + VALUES (?, ?, ?, ?, ?) """, - (user.id, user_name, admin_id), + (user.id, user_name, admin_id, email, password), ) await db.execute( diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index fbd13e725..2f516b283 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -48,6 +48,26 @@ + + + + GET + /usermanager/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
@@ -114,7 +134,8 @@ {"admin_id": <string>, "user_name": <string>, - "wallet_name": <string>}
Returns 201 CREATED (application/json) @@ -128,7 +149,8 @@ curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{ g.user.id }}", "wallet_name": <string>, "user_name": - <string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + <string>, "email": <Optional string>, "password": < + Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H "Content-type: application/json" diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html index 44837a547..06e5423ab 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -157,6 +157,18 @@ v-model.trim="userDialog.data.walname" label="Initial wallet name" > + + {% endblock %} {% block scripts %} {{ window_vars(user) }}