From 32ca6289df6f3b4fe89899be8142dbe0cc94f5a5 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 19 Sep 2021 09:31:16 +0200 Subject: [PATCH 01/15] fix: broken payment API implementation --- lnbits/core/views/api.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 8d086bd3..4e0d1d6f 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -52,6 +52,7 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): class CreateInvoiceData(BaseModel): + out: Optional[bool] = True amount: int = Query(None, ge=1) memo: str = None unit: Optional[str] = None @@ -60,6 +61,7 @@ class CreateInvoiceData(BaseModel): lnurl_balance_check: Optional[str] = None extra: Optional[dict] = None webhook: Optional[str] = None + bolt11: Optional[str] = None async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): if "description_hash" in data: @@ -169,17 +171,16 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet): @core_app.post("/api/v1/payments", deprecated=True, description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead", status_code=HTTPStatus.CREATED) -async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True, - invoiceData: Optional[CreateInvoiceData] = Body(None), - bolt11: Optional[str] = Body(None)): +async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), + invoiceData: CreateInvoiceData = Body(...)): if wallet.wallet_type < 0 or wallet.wallet_type > 2: raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid") - if out is True and wallet.wallet_type == 0: - if not bolt11: + if invoiceData.out is True and wallet.wallet_type == 0: + if not invoiceData.bolt11: raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="BOLT11 string is invalid or not given") - return await api_payments_pay_invoice(bolt11, wallet.wallet) # admin key + return await api_payments_pay_invoice(invoiceData.bolt11, wallet.wallet) # admin key return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key class CreateLNURLData(BaseModel): From c906421088599ffa603c3483deadb6a05ce0d281 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 19 Sep 2021 13:24:19 +0200 Subject: [PATCH 02/15] fix: add Jinja2 as an dependency --- Pipfile | 1 + Pipfile.lock | 182 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 122 insertions(+), 61 deletions(-) diff --git a/Pipfile b/Pipfile index 2b8a67f7..af3e4174 100644 --- a/Pipfile +++ b/Pipfile @@ -27,6 +27,7 @@ asyncio = "*" fastapi = "*" uvicorn = {extras = ["standard"], version = "*"} sse-starlette = "*" +jinja2 = "3.0.1" [dev-packages] black = "==20.8b1" diff --git a/Pipfile.lock b/Pipfile.lock index 98d92adb..907c539e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e26f678c4b89a86400e0a62396d06e360bfdf1e0f922d474ded200ee1ffde5c4" + "sha256": "97473b3cb250742ebabd8c3a71d4e4c42f8feeaff49dd4542cae24429f096535" }, "pipfile-spec": 6, "requires": { @@ -26,11 +26,11 @@ }, "anyio": { "hashes": [ - "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0", - "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374" + "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe", + "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.3.0" + "version": "==3.3.1" }, "asgiref": { "hashes": [ @@ -91,11 +91,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", + "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" ], "markers": "python_version >= '3.5'", - "version": "==2.0.4" + "version": "==2.0.6" }, "click": { "hashes": [ @@ -115,10 +115,10 @@ }, "embit": { "hashes": [ - "sha256:19f69929caf0d2fcfd4b708dd873384dfc36267944d02d5e6dfebc835f294e1b" + "sha256:992332bd89af6e2d027e26fe437eb14aa33997db08c882c49064d49c3e6f4ab9" ], "index": "pypi", - "version": "==0.4.6" + "version": "==0.4.9" }, "environs": { "hashes": [ @@ -146,11 +146,11 @@ }, "httpcore": { "hashes": [ - "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", - "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff" + "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3", + "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0" ], "markers": "python_version >= '3.6'", - "version": "==0.13.6" + "version": "==0.13.7" }, "httptools": { "hashes": [ @@ -195,6 +195,14 @@ "markers": "python_version < '3.8'", "version": "==4.8.1" }, + "jinja2": { + "hashes": [ + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + ], + "index": "pypi", + "version": "==3.0.1" + }, "lnurl": { "hashes": [ "sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92", @@ -203,6 +211,66 @@ "index": "pypi", "version": "==0.3.6" }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, "marshmallow": { "hashes": [ "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", @@ -457,12 +525,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc", - "sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3", - "sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", - "version": "==3.10.0.1" + "version": "==3.10.0.2" }, "uvicorn": { "extras": [ @@ -505,41 +573,33 @@ }, "websockets": { "hashes": [ - "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc", - "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e", - "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135", - "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02", - "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3", - "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf", - "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b", - "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2", - "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af", - "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d", - "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880", - "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077", - "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f", - "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec", - "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25", - "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0", - "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe", - "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a", - "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb", - "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d", - "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857", - "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c", - "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0", - "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40", - "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4", - "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20", - "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314", - "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da", - "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58", - "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2", - "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd", - "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a", - "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd" + "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8", + "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b", + "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539", + "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939", + "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4", + "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80", + "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474", + "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76", + "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a", + "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37", + "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238", + "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379", + "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805", + "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7", + "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537", + "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456", + "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c", + "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002", + "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567", + "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da", + "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a", + "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368", + "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2", + "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1", + "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465" ], - "version": "==9.1" + "version": "==10.0" }, "zipp": { "hashes": [ @@ -707,11 +767,11 @@ }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "py": { "hashes": [ @@ -731,11 +791,11 @@ }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], "index": "pypi", - "version": "==6.2.4" + "version": "==6.2.5" }, "pytest-cov": { "hashes": [ @@ -837,12 +897,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc", - "sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3", - "sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", - "version": "==3.10.0.1" + "version": "==3.10.0.2" }, "zipp": { "hashes": [ From c5d96d21e9c61a0bc87e0fb495abce4166326085 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 19 Sep 2021 13:25:39 +0200 Subject: [PATCH 03/15] fix: core pay via lnurl endpoint --- lnbits/core/views/api.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 4e0d1d6f..2fc86095 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -190,8 +190,9 @@ class CreateLNURLData(BaseModel): comment: Optional[str] = None description: Optional[str] = None -@core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())]) -async def api_payments_pay_lnurl(data: CreateLNURLData): +@core_app.post("/api/v1/payments/lnurl") +async def api_payments_pay_lnurl(data: CreateLNURLData, + wallet: WalletTypeInfo = Depends(get_key_type)): domain = urlparse(data.callback).netloc async with httpx.AsyncClient() as client: @@ -221,13 +222,13 @@ async def api_payments_pay_lnurl(data: CreateLNURLData): if invoice.amount_msat != data.amount: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=f"{domain} returned an invalid invoice. Expected {g().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 != g().data["description_hash"]: + if invoice.description_hash != data.description_hash: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=f"{domain} returned an invalid invoice. Expected description_hash == {g().data['description_hash']}, got {invoice.description_hash}." + detail=f"{domain} returned an invalid invoice. Expected description_hash == {data['description_hash']}, got {invoice.description_hash}." ) @@ -239,7 +240,7 @@ async def api_payments_pay_lnurl(data: CreateLNURLData): extra["comment"] = data.comment payment_hash = await pay_invoice( - wallet_id=g().wallet.id, + wallet_id=wallet.wallet.id, payment_request=params["pr"], description=data.description, extra=extra, From a5c0a2bb5078da5d767a27ed1bb3cebecd03e3cc Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 19 Sep 2021 13:34:31 +0200 Subject: [PATCH 04/15] fix: offlineshop ext lnurl handling --- lnbits/extensions/offlineshop/lnurl.py | 13 +++++++------ lnbits/extensions/offlineshop/models.py | 8 +++++++- lnbits/extensions/offlineshop/views.py | 17 ++++++++++------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index 72ef2a42..b04d8512 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -1,4 +1,5 @@ import hashlib +from lnbits.extensions.offlineshop.models import Item from fastapi.params import Query from starlette.requests import Request @@ -13,8 +14,8 @@ from .crud import get_shop, get_item @offlineshop_ext.get("/lnurl/{item_id}", name="offlineshop.lnurl_response") -async def lnurl_response(item_id: int = Query(...)): - item = await get_item(item_id) +async def lnurl_response(req: Request, item_id: int = Query(...)): + item = await get_item(item_id) # type: Item if not item: return {"status": "ERROR", "reason": "Item not found."} @@ -28,7 +29,7 @@ async def lnurl_response(item_id: int = Query(...)): ) * 1000 resp = LnurlPayResponse( - callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True), + callback=req.url_for("offlineshop.lnurl_callback", item_id=item.id), min_sendable=price_msat, max_sendable=price_msat, metadata=await item.lnurlpay_metadata(), @@ -37,9 +38,9 @@ async def lnurl_response(item_id: int = Query(...)): return resp.dict() -@offlineshop_ext.get("/lnurl/cb/") +@offlineshop_ext.get("/lnurl/cb/{item_id}", name="offlineshop.lnurl_callback") async def lnurl_callback(request: Request, item_id: int): - item = await get_item(item_id) + item = await get_item(item_id) # type: Item if not item: return {"status": "ERROR", "reason": "Couldn't find item."} @@ -52,7 +53,7 @@ async def lnurl_callback(request: Request, item_id: int): min = price * 995 max = price * 1010 - amount_received = int(request.args.get("amount") or 0) + amount_received = int(request.query_params.get("amount") or 0) if amount_received < min: return LnurlErrorResponse( reason=f"Amount {amount_received} is smaller than minimum {min}." diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 53fdb845..12616022 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -14,7 +14,8 @@ from .helpers import totp shop_counters: Dict = {} -class ShopCounter(BaseModel): +class ShopCounter(): + wordlist: List[str] fulfilled_payments: OrderedDict counter: int @@ -88,6 +89,11 @@ class Item(BaseModel): price: int unit: str + def lnurl(self, req: Request) -> str: + return lnurl_encode( + req.url_for("offlineshop.lnurl_response", item_id=self.id) + ) + def values(self, req: Request): values = self.dict() values["lnurl"] = lnurl_encode( diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index 80870b27..e8bea173 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -1,8 +1,9 @@ import time from datetime import datetime from http import HTTPStatus +from typing import List -from fastapi.params import Depends +from fastapi.params import Depends, Query from starlette.responses import HTMLResponse from lnbits.decorators import check_user_exists @@ -10,6 +11,7 @@ from lnbits.core.models import Payment, User from lnbits.core.crud import get_standalone_payment from . import offlineshop_ext, offlineshop_renderer +from .models import Item from .crud import get_item, get_shop from fastapi import Request, HTTPException @@ -20,14 +22,14 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @offlineshop_ext.get("/print", response_class=HTMLResponse) -async def print_qr_codes(request: Request): +async def print_qr_codes(request: Request, items: List[int] = None): items = [] - for item_id in request.args.get("items").split(","): - item = await get_item(item_id) + for item_id in request.query_params.get("items").split(","): + item = await get_item(item_id) # type: Item if item: items.append( { - "lnurl": item.lnurl, + "lnurl": item.lnurl(request), "name": item.name, "price": f"{item.price} {item.unit}", } @@ -36,8 +38,9 @@ async def print_qr_codes(request: Request): return offlineshop_renderer().TemplateResponse("offlineshop/print.html", {"request": request,"items":items}) -@offlineshop_ext.get("/confirmation") -async def confirmation_code(p: str): +@offlineshop_ext.get("/confirmation/{p}", name="offlineshop.confirmation_code", + response_class=HTMLResponse) +async def confirmation_code(p: str = Query(...)): style = "" payment_hash = p From 0891611d2b647ee5ef3318758a71c7aa4bc1c837 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 27 Sep 2021 15:52:08 +0100 Subject: [PATCH 05/15] fastAPI port views_api --- lnbits/extensions/lnticket/views_api.py | 136 +++++++++++++++--------- 1 file changed, 88 insertions(+), 48 deletions(-) diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index 129af411..f1094e7e 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -3,9 +3,13 @@ from quart import g, jsonify, request from http import HTTPStatus from fastapi import FastAPI, Query +from fastapi.params import Depends from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse + from pydantic import BaseModel +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse # type: ignore from lnbits.core.crud import get_user, get_wallet from lnbits.core.services import create_invoice, check_invoice_status @@ -29,17 +33,16 @@ from .crud import ( # FORMS -@lnticket_ext.get("/api/v1/forms") -@api_check_wallet_key("invoice") -async def api_forms(): - wallet_ids = [g.wallet.id] +@lnticket_ext.get("/api/v1/forms", status_code=HTTPStatus.OK) +# @api_check_wallet_key("invoice") +async def api_forms(r: Request, wallet: WalletTypeInfo = Depends(get_key_type)): + wallet_ids = [wallet.wallet.id] - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids + if "all_wallets" in r.args: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids return ( [form._asdict() for form in await get_forms(wallet_ids)], - HTTPStatus.OK, ) class CreateData(BaseModel): @@ -50,9 +53,9 @@ class CreateData(BaseModel): amount: int = Query(..., ge=0) flatrate: int = Query(...) -@lnticket_ext.post("/api/v1/forms") +@lnticket_ext.post("/api/v1/forms", status_code=HTTPStatus.CREATED) @lnticket_ext.put("/api/v1/forms/{form_id}") -@api_check_wallet_key("invoice") +# @api_check_wallet_key("invoice") # @api_validate_post_request( # schema={ # "wallet": {"type": "string", "empty": False, "required": True}, @@ -63,52 +66,68 @@ class CreateData(BaseModel): # "flatrate": {"type": "integer", "required": True}, # } # ) -async def api_form_create(data: CreateData, form_id=None): +async def api_form_create(data: CreateData, form_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): if form_id: form = await get_form(form_id) if not form: - return {"message": "Form does not exist."}, HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Form does not exist." + ) + # return {"message": "Form does not exist."}, HTTPStatus.NOT_FOUND - if form.wallet != g.wallet.id: - return jsonify{"message": "Not your form."}, HTTPStatus.FORBIDDEN + if form.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Not your form." + ) + # return {"message": "Not your form."}, HTTPStatus.FORBIDDEN form = await update_form(form_id, **data) else: form = await create_form(**data) - return form._asdict(), HTTPStatus.CREATED + return form._asdict() @lnticket_ext.delete("/api/v1/forms/{form_id}") -@api_check_wallet_key("invoice") -async def api_form_delete(form_id): +# @api_check_wallet_key("invoice") +async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type)): form = await get_form(form_id) if not form: - return {"message": "Form does not exist."}, HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Form does not exist." + ) + # return {"message": "Form does not exist."}, HTTPStatus.NOT_FOUND - if form.wallet != g.wallet.id: - return {"message": "Not your form."}, HTTPStatus.FORBIDDEN + if form.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Not your form." + ) + # return {"message": "Not your form."}, HTTPStatus.FORBIDDEN await delete_form(form_id) - return "", HTTPStatus.NO_CONTENT + # return "", HTTPStatus.NO_CONTENT + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) #########tickets########## -@lnticket_ext.get("/api/v1/tickets") -@api_check_wallet_key("invoice") -async def api_tickets(all_wallets: bool = Query(None)): - wallet_ids = [g.wallet.id] +@lnticket_ext.get("/api/v1/tickets", status_code=HTTPStatus.OK) +# @api_check_wallet_key("invoice") +async def api_tickets(all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)): + wallet_ids = [wallet.wallet.id] if all_wallets: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids return ( - [form._asdict() for form in await get_tickets(wallet_ids)], - HTTPStatus.OK, + [form._asdict() for form in await get_tickets(wallet_ids)] ) class CreateTicketData(BaseModel): @@ -118,7 +137,7 @@ class CreateTicketData(BaseModel): ltext: str = Query(...) sats: int = Query(..., ge=0) -@lnticket_ext.post("/api/v1/tickets/{form_id}") +@lnticket_ext.post("/api/v1/tickets/{form_id}", status_code=HTTPStatus.CREATED) # @api_validate_post_request( # schema={ # "form": {"type": "string", "empty": False, "required": True}, @@ -131,7 +150,11 @@ class CreateTicketData(BaseModel): async def api_ticket_make_ticket(data: CreateTicketData, form_id): form = await get_form(form_id) if not form: - return {"message": "LNTicket does not exist."}, HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"LNTicket does not exist." + ) + # return {"message": "LNTicket does not exist."}, HTTPStatus.NOT_FOUND nwords = len(re.split(r"\s+", data["ltext"])) sats = data["sats"] @@ -144,53 +167,70 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id): extra={"tag": "lnticket"}, ) except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(e) + ) + # return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR ticket = await create_ticket( payment_hash=payment_hash, wallet=form.wallet, **data ) if not ticket: - return ( - {"message": "LNTicket could not be fetched."}, - HTTPStatus.NOT_FOUND, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNTicket could not be fetched." ) + # return ( + # {"message": "LNTicket could not be fetched."}, + # HTTPStatus.NOT_FOUND, + # ) - return - {"payment_hash": payment_hash, "payment_request": payment_request}, - HTTPStatus.OK + return { + "payment_hash": payment_hash, + "payment_request": payment_request + } -@lnticket_ext.get("/api/v1/tickets/{payment_hash}") +@lnticket_ext.get("/api/v1/tickets/{payment_hash}", status_code=HTTPStatus.OK) async def api_ticket_send_ticket(payment_hash): ticket = await get_ticket(payment_hash) try: status = await check_invoice_status(ticket.wallet, payment_hash) is_paid = not status.pending except Exception: - return {"paid": False}, HTTPStatus.OK + return {"paid": False} if is_paid: wallet = await get_wallet(ticket.wallet) payment = await wallet.get_payment(payment_hash) await payment.set_pending(False) ticket = await set_ticket_paid(payment_hash=payment_hash) - return {"paid": True}, HTTPStatus.OK + return {"paid": True} - return {"paid": False}, HTTPStatus.OK + return {"paid": False} @lnticket_ext.delete("/api/v1/tickets/{ticket_id}") -@api_check_wallet_key("invoice") -async def api_ticket_delete(ticket_id): +# @api_check_wallet_key("invoice") +async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)): ticket = await get_ticket(ticket_id) if not ticket: - return {"message": "Paywall does not exist."}, HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"LNTicket does not exist." + ) + # return {"message": "Paywall does not exist."}, HTTPStatus.NOT_FOUND - if ticket.wallet != g.wallet.id: - return {"message": "Not your ticket."}, HTTPStatus.FORBIDDEN + if ticket.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your ticket." + ) + # return {"message": "Not your ticket."}, HTTPStatus.FORBIDDEN await delete_ticket(ticket_id) - - return "", HTTPStatus.NO_CONTENT + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + # return "" From 3a3ef4cd2217a3ede94e776323fb2295b9da827d Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 27 Sep 2021 20:33:18 +0100 Subject: [PATCH 06/15] tickets fastAPI: init --- lnbits/extensions/lnticket/__init__.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index cfdadc40..c3b47fb0 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -1,12 +1,25 @@ -from quart import Blueprint +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount + from lnbits.db import Database +from lnbits.helpers import template_renderer db = Database("ext_lnticket") -lnticket_ext: Blueprint = Blueprint( - "lnticket", __name__, static_folder="static", template_folder="templates" +lnticket_ext: APIRouter = APIRouter( + prefix="/lnticket", + tags=["LNTicket"] + # "lnticket", __name__, static_folder="static", template_folder="templates" ) +def lnticket_renderer(): + return template_renderer( + [ + "lnbits/extensions/lnticket/templates", + ] + ) + from .views_api import * # noqa from .views import * # noqa From ca5ee4ed4fb72b186bb31f81d7e31d99501e974d Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 27 Sep 2021 21:02:50 +0100 Subject: [PATCH 07/15] tickets fastAPI --- lnbits/extensions/lnticket/__init__.py | 7 +++---- lnbits/extensions/lnticket/views.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index c3b47fb0..28ef8067 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -23,8 +23,7 @@ def lnticket_renderer(): 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)) +@lntickets_ext.on_event("startup") +def _do_it(): + register_listeners() diff --git a/lnbits/extensions/lnticket/views.py b/lnbits/extensions/lnticket/views.py index 69417be5..dbf3cbbd 100644 --- a/lnbits/extensions/lnticket/views.py +++ b/lnbits/extensions/lnticket/views.py @@ -4,29 +4,33 @@ from lnbits.core.crud import get_wallet from lnbits.decorators import check_user_exists, validate_uuids from http import HTTPStatus -from . import lnticket_ext +from . import lnticket_ext, lnticket_renderer from .crud import get_form from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") -@lnticket_ext.route("/") +@lnticket_ext.get("/", response_class=HTMLResponse) @validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("lnticket/index.html", {"request": request,"user":g.user}) +# @check_user_exists() +async def index(request: Request, user: User = Depends(check_user_exists)): + return lnticket_renderer().TemplateResponse("lnticket/index.html", {"request": request,"user": user.dict()}) -@lnticket_ext.route("/") +@lnticket_ext.get("/{form_id}") async def display(request: Request, form_id): form = await get_form(form_id) if not form: - abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNTicket does not exist." + ) + # abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.") wallet = await get_wallet(form.wallet) - return await templates.TemplateResponse( + return lnticket_renderer().TemplateResponse( "lnticket/display.html", {"request": request, "form_id":form.id, From 8c5989d4655179f9d3f2c1818e87d6789f315002 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 28 Sep 2021 21:10:51 +0200 Subject: [PATCH 08/15] fix: crash when an ext doesn't have static files --- lnbits/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index 288caa5b..b0289b93 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -96,9 +96,10 @@ def register_routes(app: FastAPI) -> None: ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}") ext_route = getattr(ext_module, f"{ext.code}_ext") - ext_statics = getattr(ext_module, f"{ext.code}_static_files") - for s in ext_statics: - app.mount(s["path"], s["app"], s["name"]) + if hasattr(ext_module, f"{ext.code}_static_files"): + ext_statics = getattr(ext_module, f"{ext.code}_static_files") + for s in ext_statics: + app.mount(s["path"], s["app"], s["name"]) app.include_router(ext_route) except Exception as e: From 2a4314ca8490dfbc936790d3c95a22213686b73e Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 28 Sep 2021 21:13:04 +0200 Subject: [PATCH 09/15] fix: raise bad req error on call without X-API-key --- lnbits/decorators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index ff42d0fd..440f8fa1 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -96,6 +96,8 @@ async def get_key_type(r: Request, await checker.__call__(r) return WalletTypeInfo(0, checker.wallet) except HTTPException as e: + if e.status_code == HTTPStatus.BAD_REQUEST: + raise if e.status_code == HTTPStatus.UNAUTHORIZED: pass except: @@ -106,6 +108,8 @@ async def get_key_type(r: Request, await checker.__call__(r) return WalletTypeInfo(1, checker.wallet) except HTTPException as e: + if e.status_code == HTTPStatus.BAD_REQUEST: + raise if e.status_code == HTTPStatus.UNAUTHORIZED: return WalletTypeInfo(2, None) except: From 85d9a3fcc375d17262c69e04d21bd7bd0e1a3023 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 28 Sep 2021 21:18:15 +0200 Subject: [PATCH 10/15] fix: some errors in lnticket ext --- lnbits/extensions/lnticket/__init__.py | 8 ++--- lnbits/extensions/lnticket/crud.py | 14 +++----- lnbits/extensions/lnticket/models.py | 18 +++++++++- lnbits/extensions/lnticket/tasks.py | 20 +++++------ lnbits/extensions/lnticket/views.py | 12 ++++--- lnbits/extensions/lnticket/views_api.py | 44 ++++++++----------------- 6 files changed, 57 insertions(+), 59 deletions(-) diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index 28ef8067..0a872e8c 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -1,6 +1,4 @@ -from fastapi import APIRouter, FastAPI -from fastapi.staticfiles import StaticFiles -from starlette.routing import Mount +from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer @@ -23,7 +21,9 @@ def lnticket_renderer(): from .views_api import * # noqa from .views import * # noqa +from .tasks import register_listeners -@lntickets_ext.on_event("startup") +@lnticket_ext.on_event("startup") def _do_it(): + # FIXME: isn't called yet register_listeners() diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py index 5c1f1e02..327fb90e 100644 --- a/lnbits/extensions/lnticket/crud.py +++ b/lnbits/extensions/lnticket/crud.py @@ -1,9 +1,10 @@ +from lnbits.core.models import Wallet from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Tickets, Forms +from .models import CreateFormData, Tickets, Forms import httpx @@ -103,13 +104,8 @@ async def delete_ticket(ticket_id: str) -> None: async def create_form( - *, - wallet: str, - name: str, - webhook: Optional[str] = None, - description: str, - amount: int, - flatrate: int, + data: CreateFormData, + wallet: Wallet, ) -> Forms: form_id = urlsafe_short_hash() await db.execute( @@ -117,7 +113,7 @@ async def create_form( INSERT INTO lnticket.form2 (id, wallet, name, webhook, description, flatrate, amount, amountmade) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, - (form_id, wallet, name, webhook, description, flatrate, amount, 0), + (form_id, wallet.id, wallet.name, data.webhook, data.description, data.flatrate, data.amount, 0), ) form = await get_form(form_id) diff --git a/lnbits/extensions/lnticket/models.py b/lnbits/extensions/lnticket/models.py index 1bc8237c..6fac90c2 100644 --- a/lnbits/extensions/lnticket/models.py +++ b/lnbits/extensions/lnticket/models.py @@ -1,10 +1,26 @@ +from typing import Optional +from fastapi.param_functions import Query from pydantic import BaseModel +class CreateFormData(BaseModel): + name: str = Query(...) + webhook: str = Query(None) + description: str = Query(..., min_length=0) + amount: int = Query(..., ge=0) + flatrate: int = Query(...) + +class CreateTicketData(BaseModel): + form: str = Query(...) + name: str = Query(...) + email: str = Query("") + ltext: str = Query(...) + sats: int = Query(..., ge=0) + class Forms(BaseModel): id: str wallet: str name: str - webhook: str + webhook: Optional[str] description: str amount: int flatrate: int diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py index 5160de1d..d08d74c9 100644 --- a/lnbits/extensions/lnticket/tasks.py +++ b/lnbits/extensions/lnticket/tasks.py @@ -1,23 +1,21 @@ -import json -import trio # type: ignore +import asyncio 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 lnbits.tasks import register_invoice_listener 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) + send_queue = asyncio.Queue() + recv_queue = asyncio.Queue() + register_invoice_listener(send_queue) + await wait_for_paid_invoices(recv_queue) -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: +async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): + while True: + payment = await invoice_paid_queue.get() await on_invoice_paid(payment) diff --git a/lnbits/extensions/lnticket/views.py b/lnbits/extensions/lnticket/views.py index dbf3cbbd..62002b76 100644 --- a/lnbits/extensions/lnticket/views.py +++ b/lnbits/extensions/lnticket/views.py @@ -1,7 +1,9 @@ -from quart import g, abort, render_template - +from fastapi.param_functions import Depends +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User from lnbits.core.crud import get_wallet -from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.decorators import check_user_exists from http import HTTPStatus from . import lnticket_ext, lnticket_renderer @@ -12,7 +14,9 @@ from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") @lnticket_ext.get("/", response_class=HTMLResponse) -@validate_uuids(["usr"], required=True) +# not needed as we automatically get the user with the given ID +# If no user with this ID is found, an error is raised +# @validate_uuids(["usr"], required=True) # @check_user_exists() async def index(request: Request, user: User = Depends(check_user_exists)): return lnticket_renderer().TemplateResponse("lnticket/index.html", {"request": request,"user": user.dict()}) diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index f1094e7e..eaf73e93 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -1,10 +1,10 @@ +from lnbits.extensions.lnticket.models import CreateFormData, CreateTicketData import re -from quart import g, jsonify, request from http import HTTPStatus +from typing import List -from fastapi import FastAPI, Query +from fastapi import Query from fastapi.params import Depends -from fastapi.encoders import jsonable_encoder from pydantic import BaseModel from starlette.exceptions import HTTPException @@ -13,7 +13,7 @@ from starlette.responses import HTMLResponse, JSONResponse # type: ignore from lnbits.core.crud import get_user, get_wallet from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.decorators import WalletTypeInfo, get_key_type from . import lnticket_ext from .crud import ( @@ -33,26 +33,17 @@ from .crud import ( # FORMS -@lnticket_ext.get("/api/v1/forms", status_code=HTTPStatus.OK) -# @api_check_wallet_key("invoice") -async def api_forms(r: Request, wallet: WalletTypeInfo = Depends(get_key_type)): +@lnticket_ext.get("/api/v1/forms") +async def api_forms_get(r: Request, all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)): wallet_ids = [wallet.wallet.id] - if "all_wallets" in r.args: + if all_wallets: wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids return ( - [form._asdict() for form in await get_forms(wallet_ids)], + [form.dict() for form in await get_forms(wallet_ids)], ) -class CreateData(BaseModel): - wallet: str = Query(...) - name: str = Query(...) - webhook: str = Query(None) - description: str = Query(..., min_length=0) - amount: int = Query(..., ge=0) - flatrate: int = Query(...) - @lnticket_ext.post("/api/v1/forms", status_code=HTTPStatus.CREATED) @lnticket_ext.put("/api/v1/forms/{form_id}") # @api_check_wallet_key("invoice") @@ -66,7 +57,7 @@ class CreateData(BaseModel): # "flatrate": {"type": "integer", "required": True}, # } # ) -async def api_form_create(data: CreateData, form_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_form_create(data: CreateFormData, form_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): if form_id: form = await get_form(form_id) @@ -86,8 +77,8 @@ async def api_form_create(data: CreateData, form_id=None, wallet: WalletTypeInfo form = await update_form(form_id, **data) else: - form = await create_form(**data) - return form._asdict() + form = await create_form(data, wallet.wallet) + return form.dict() @lnticket_ext.delete("/api/v1/forms/{form_id}") @@ -118,25 +109,18 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type #########tickets########## -@lnticket_ext.get("/api/v1/tickets", status_code=HTTPStatus.OK) +@lnticket_ext.get("/api/v1/tickets") # @api_check_wallet_key("invoice") -async def api_tickets(all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_tickets(all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)): wallet_ids = [wallet.wallet.id] if all_wallets: wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids return ( - [form._asdict() for form in await get_tickets(wallet_ids)] + [form.dict() for form in await get_tickets(wallet_ids)] ) -class CreateTicketData(BaseModel): - form: str = Query(...) - name: str = Query(...) - email: str = Query("") - ltext: str = Query(...) - sats: int = Query(..., ge=0) - @lnticket_ext.post("/api/v1/tickets/{form_id}", status_code=HTTPStatus.CREATED) # @api_validate_post_request( # schema={ From f827d2ce181d97368161d46ab8de2e9f061b9872 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Tue, 28 Sep 2021 22:03:45 +0200 Subject: [PATCH 11/15] fix: fetch tickets and forms JS and endpoints --- lnbits/extensions/lnticket/templates/lnticket/index.html | 4 ++-- lnbits/extensions/lnticket/views_api.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html index bc9fe9a4..1607925b 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/index.html +++ b/lnbits/extensions/lnticket/templates/lnticket/index.html @@ -337,7 +337,7 @@ LNbits.api .request( 'GET', - '/lnticket/api/v1/tickets?all_wallets', + '/lnticket/api/v1/tickets?all_wallets=true', this.g.user.wallets[0].inkey ) .then(function (response) { @@ -382,7 +382,7 @@ LNbits.api .request( 'GET', - '/lnticket/api/v1/forms?all_wallets', + '/lnticket/api/v1/forms?all_wallets=true', this.g.user.wallets[0].inkey ) .then(function (response) { diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index eaf73e93..8ead81a3 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -40,9 +40,7 @@ async def api_forms_get(r: Request, all_wallets: bool = Query(False), wallet: Wa if all_wallets: wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids - return ( - [form.dict() for form in await get_forms(wallet_ids)], - ) + return [form.dict() for form in await get_forms(wallet_ids)] @lnticket_ext.post("/api/v1/forms", status_code=HTTPStatus.CREATED) @lnticket_ext.put("/api/v1/forms/{form_id}") @@ -117,9 +115,7 @@ async def api_tickets(all_wallets: bool = Query(False), wallet: WalletTypeInfo = if all_wallets: wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids - return ( - [form.dict() for form in await get_tickets(wallet_ids)] - ) + return [form.dict() for form in await get_tickets(wallet_ids)] @lnticket_ext.post("/api/v1/tickets/{form_id}", status_code=HTTPStatus.CREATED) # @api_validate_post_request( From 534dfdb4e0f6d64ddfb2828cb0637c5a5e14e47d Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Wed, 29 Sep 2021 20:44:00 +0200 Subject: [PATCH 12/15] fix: startup of lntickets listeners --- lnbits/app.py | 4 ++++ lnbits/extensions/lnticket/__init__.py | 14 +++++++++----- lnbits/extensions/lnticket/tasks.py | 12 ++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index b0289b93..a37a5443 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -96,6 +96,10 @@ def register_routes(app: FastAPI) -> None: ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}") ext_route = getattr(ext_module, f"{ext.code}_ext") + if hasattr(ext_module, f"{ext.code}_start"): + ext_start_func = getattr(ext_module, f"{ext.code}_start") + ext_start_func() + if hasattr(ext_module, f"{ext.code}_static_files"): ext_statics = getattr(ext_module, f"{ext.code}_static_files") for s in ext_statics: diff --git a/lnbits/extensions/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py index 0a872e8c..9394a1c9 100644 --- a/lnbits/extensions/lnticket/__init__.py +++ b/lnbits/extensions/lnticket/__init__.py @@ -1,7 +1,10 @@ +import asyncio + from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_lnticket") @@ -21,9 +24,10 @@ def lnticket_renderer(): from .views_api import * # noqa from .views import * # noqa -from .tasks import register_listeners +from .tasks import wait_for_paid_invoices + + +def lnticket_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) -@lnticket_ext.on_event("startup") -def _do_it(): - # FIXME: isn't called yet - register_listeners() diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py index d08d74c9..89e5ebf5 100644 --- a/lnbits/extensions/lnticket/tasks.py +++ b/lnbits/extensions/lnticket/tasks.py @@ -6,16 +6,12 @@ from lnbits.tasks import register_invoice_listener from .crud import get_ticket, set_ticket_paid -async def register_listeners(): - send_queue = asyncio.Queue() - recv_queue = asyncio.Queue() - register_invoice_listener(send_queue) - await wait_for_paid_invoices(recv_queue) +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) - -async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): while True: - payment = await invoice_paid_queue.get() + payment = await invoice_queue.get() await on_invoice_paid(payment) From 87c784aa0cd34e4da91f73179f09c19d7bf3c698 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Wed, 29 Sep 2021 20:57:35 +0200 Subject: [PATCH 13/15] fix: create and pay a ticket code --- lnbits/extensions/lnticket/crud.py | 10 +++------- lnbits/extensions/lnticket/views_api.py | 7 +++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py index 327fb90e..e391abb5 100644 --- a/lnbits/extensions/lnticket/crud.py +++ b/lnbits/extensions/lnticket/crud.py @@ -4,25 +4,21 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import CreateFormData, Tickets, Forms +from .models import CreateFormData, CreateTicketData, Tickets, Forms import httpx async def create_ticket( payment_hash: str, wallet: str, - form: str, - name: str, - email: str, - ltext: str, - sats: int, + data: CreateTicketData ) -> Tickets: await db.execute( """ INSERT INTO lnticket.ticket (id, form, email, ltext, name, wallet, sats, paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, - (payment_hash, form, email, ltext, name, wallet, sats, False), + (payment_hash, data.form, data.email, data.ltext, data.name, wallet, data.sats, False), ) ticket = await get_ticket(payment_hash) diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index 8ead81a3..1474518b 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -136,13 +136,12 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id): ) # return {"message": "LNTicket does not exist."}, HTTPStatus.NOT_FOUND - nwords = len(re.split(r"\s+", data["ltext"])) - sats = data["sats"] + nwords = len(re.split(r"\s+", data.ltext)) try: payment_hash, payment_request = await create_invoice( wallet_id=form.wallet, - amount=sats, + amount=data.sats, memo=f"ticket with {nwords} words on {form_id}", extra={"tag": "lnticket"}, ) @@ -154,7 +153,7 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id): # return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR ticket = await create_ticket( - payment_hash=payment_hash, wallet=form.wallet, **data + payment_hash=payment_hash, wallet=form.wallet, data=data ) if not ticket: From 27e17814648a66b69d949f2a724805af9f2aa05a Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Thu, 30 Sep 2021 18:52:37 +0200 Subject: [PATCH 14/15] chore: update requirements.txt --- requirements.txt | 77 +++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/requirements.txt b/requirements.txt index af1b4938..8bfb35bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,51 +1,48 @@ -aiofiles==0.6.0 -async-generator==1.10 -attrs==20.3.0 +aiofiles==0.7.0 +anyio==3.3.1 +asgiref==3.4.1 +asyncio==3.4.3 +attrs==21.2.0 bech32==1.2.0 -bitstring==3.1.7 -blinker==1.4 -brotli==1.0.9 -cerberus==1.3.3 -certifi==2020.12.5 -click==7.1.2 -ecdsa==0.16.1 -embit==0.2.1 -environs==9.3.2 +bitstring==3.1.9 +cerberus==1.3.4 +certifi==2021.5.30 +charset-normalizer==2.0.6 +click==8.0.1 +ecdsa==0.17.0 +embit==0.4.9 +environs==9.3.3 +fastapi==0.68.1 h11==0.12.0 -h2==4.0.0 -hpack==4.0.0 -httpcore==0.12.3 -httpx==0.17.1 -hypercorn==0.11.2 -hyperframe==6.0.0 -idna==3.1 -itsdangerous==1.1.0 -jinja2==2.11.3 +httpcore==0.13.7 +httptools==0.2.0 +httpx==0.19.0 +idna==3.2 +importlib-metadata==4.8.1 +jinja2==3.0.1 lnurl==0.3.6 -markupsafe==1.1.1 -marshmallow==3.11.1 +markupsafe==2.0.1 +marshmallow==3.13.0 outcome==1.1.0 -priority==1.3.0 -pydantic==1.8.1 -pyngrok==5.0.5 -pypng==0.0.20 +psycopg2-binary==2.9.1 +pydantic==1.8.2 +pypng==0.0.21 pyqrcode==1.2.1 pyscss==1.3.7 -python-dotenv==0.17.0 -quart==0.14.1 -quart-compress==0.2.1 -quart-cors==0.4.0 -quart-trio==0.7.0 +python-dotenv==0.19.0 +pyyaml==5.4.1 represent==1.6.0.post0 -rfc3986==1.4.0 +rfc3986==1.5.0 shortuuid==1.0.1 -six==1.15.0 +six==1.16.0 sniffio==1.2.0 -sortedcontainers==2.3.0 sqlalchemy==1.3.23 sqlalchemy-aio==0.16.0 -toml==0.10.2 -trio==0.16.0 -typing-extensions==3.7.4.3 -werkzeug==1.0.1 -wsproto==1.0.0 +sse-starlette==0.6.2 +starlette==0.14.2 +typing-extensions==3.10.0.2 +uvicorn==0.15.0 +uvloop==0.16.0 +watchgod==0.7 +websockets==10.0 +zipp==3.5.0 From dba988a969a86c77b6316afb555d5a8d8aeac39a Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Thu, 30 Sep 2021 19:16:38 +0200 Subject: [PATCH 15/15] fix: reactivate catch-all exception handler --- lnbits/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index a37a5443..8ffe4318 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -65,7 +65,7 @@ def create_app(config_object="lnbits.settings") -> FastAPI: register_routes(app) # register_commands(app) register_async_tasks(app) - # register_exception_handlers(app) + register_exception_handlers(app) return app @@ -150,11 +150,11 @@ def register_async_tasks(app): async def stop_listeners(): pass -def register_exception_handlers(app): - @app.errorhandler(Exception) +def register_exception_handlers(app: FastAPI): + @app.exception_handler(Exception) async def basic_error(request: Request, err): print("handled error", traceback.format_exc()) - etype, value, tb = sys.exc_info() + etype, _, tb = sys.exc_info() traceback.print_exception(etype, err, tb) exc = traceback.format_exc() return template_renderer().TemplateResponse("error.html", {"request": request, "err": err})