Merge pull request #328 from arcbtc/FastAPI

latest fusion44 commits
This commit is contained in:
Arc 2021-09-02 10:47:57 +01:00 committed by GitHub
commit 9e68a242e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1461 additions and 580 deletions

16
Pipfile
View File

@ -14,28 +14,22 @@ environs = "*"
lnurl = "==0.3.6" lnurl = "==0.3.6"
pyscss = "*" pyscss = "*"
shortuuid = "*" shortuuid = "*"
quart = "*"
quart-cors = "*"
quart-compress = "*"
typing-extensions = "*" typing-extensions = "*"
httpx = "*" httpx = "*"
quart-trio = "*"
trio = "==0.16.0"
sqlalchemy-aio = "*" sqlalchemy-aio = "*"
embit = "*" embit = "*"
pyqrcode = "*" pyqrcode = "*"
pypng = "*" pypng = "*"
sqlalchemy = "==1.3.23" sqlalchemy = "==1.3.23"
psycopg2-binary = "*" psycopg2-binary = "*"
aiofiles = "*"
asyncio = "*"
fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"
pytest = "*" pytest = "*"
pytest-cov = "*" pytest-cov = "*"
mypy = "latest" mypy = "latest"
pytest-trio = "*"
trio-typing = "*"
[packages.hypercorn]
extras = [ "trio",]
version = "*"

856
Pipfile.lock generated Normal file
View File

@ -0,0 +1,856 @@
{
"_meta": {
"hash": {
"sha256": "e26f678c4b89a86400e0a62396d06e360bfdf1e0f922d474ded200ee1ffde5c4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4",
"sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"
],
"index": "pypi",
"version": "==0.7.0"
},
"anyio": {
"hashes": [
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.3.0"
},
"asgiref": {
"hashes": [
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
],
"markers": "python_version >= '3.6'",
"version": "==3.4.1"
},
"asyncio": {
"hashes": [
"sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41",
"sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de",
"sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c",
"sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"
],
"index": "pypi",
"version": "==3.4.3"
},
"attrs": {
"hashes": [
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"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": [
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"bitstring": {
"hashes": [
"sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578",
"sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7",
"sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f"
],
"index": "pypi",
"version": "==3.1.9"
},
"cerberus": {
"hashes": [
"sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"
],
"index": "pypi",
"version": "==1.3.4"
},
"certifi": {
"hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
],
"version": "==2021.5.30"
},
"charset-normalizer": {
"hashes": [
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
],
"markers": "python_version >= '3.5'",
"version": "==2.0.4"
},
"click": {
"hashes": [
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.1"
},
"ecdsa": {
"hashes": [
"sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676",
"sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"
],
"index": "pypi",
"version": "==0.17.0"
},
"embit": {
"hashes": [
"sha256:19f69929caf0d2fcfd4b708dd873384dfc36267944d02d5e6dfebc835f294e1b"
],
"index": "pypi",
"version": "==0.4.6"
},
"environs": {
"hashes": [
"sha256:72b867ff7b553076cdd90f3ee01ecc1cf854987639c9c459f0ed0d3d44ae490c",
"sha256:ee5466156b50fe03aa9fec6e720feea577b5bf515d7f21b2c46608272557ba26"
],
"index": "pypi",
"version": "==9.3.3"
},
"fastapi": {
"hashes": [
"sha256:644bb815bae326575c4b2842469fb83053a4b974b82fa792ff9283d17fbbd99d",
"sha256:94d2820906c36b9b8303796fb7271337ec89c74223229e3cfcf056b5a7d59e23"
],
"index": "pypi",
"version": "==0.68.1"
},
"h11": {
"hashes": [
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"httpcore": {
"hashes": [
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.6"
},
"httptools": {
"hashes": [
"sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb",
"sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f",
"sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77",
"sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149",
"sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5",
"sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e",
"sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15",
"sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0",
"sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7",
"sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943",
"sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658",
"sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557",
"sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380",
"sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb",
"sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065"
],
"version": "==0.2.0"
},
"httpx": {
"hashes": [
"sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0",
"sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435"
],
"index": "pypi",
"version": "==0.19.0"
},
"idna": {
"hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
],
"version": "==3.2"
},
"importlib-metadata": {
"hashes": [
"sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
"sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
],
"markers": "python_version < '3.8'",
"version": "==4.8.1"
},
"lnurl": {
"hashes": [
"sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92",
"sha256:8af07460115a48f3122a5a9c9a6062bee3897d5f6ab4c9a60f6561a83a8234f6"
],
"index": "pypi",
"version": "==0.3.6"
},
"marshmallow": {
"hashes": [
"sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e",
"sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842"
],
"markers": "python_version >= '3.5'",
"version": "==3.13.0"
},
"outcome": {
"hashes": [
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
],
"markers": "python_version >= '3.6'",
"version": "==1.1.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
"sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
"sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
"sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
"sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
"sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
"sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
"sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
"sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
"sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
"sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
"sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
"sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
"sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
"sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
"sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
"sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
"sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
"sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
"sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
"sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
"sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
"sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
"sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
"sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
"sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
"sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
"sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
"sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
],
"index": "pypi",
"version": "==2.9.1"
},
"pydantic": {
"hashes": [
"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.2"
},
"pypng": {
"hashes": [
"sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"
],
"index": "pypi",
"version": "==0.0.21"
},
"pyqrcode": {
"hashes": [
"sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6",
"sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"
],
"index": "pypi",
"version": "==1.2.1"
},
"pyscss": {
"hashes": [
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
],
"index": "pypi",
"version": "==1.3.7"
},
"python-dotenv": {
"hashes": [
"sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1",
"sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"
],
"markers": "python_version >= '3.5'",
"version": "==0.19.0"
},
"pyyaml": {
"hashes": [
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
"sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
"sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
"sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
"sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
"sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
"sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
],
"version": "==5.4.1"
},
"represent": {
"hashes": [
"sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0",
"sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.6.0.post0"
},
"rfc3986": {
"extras": [
"idna2008"
],
"hashes": [
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.5.0"
},
"shortuuid": {
"hashes": [
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
],
"index": "pypi",
"version": "==1.0.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sniffio": {
"hashes": [
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"sqlalchemy": {
"hashes": [
"sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862",
"sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3",
"sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708",
"sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075",
"sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff",
"sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4",
"sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1",
"sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173",
"sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601",
"sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80",
"sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea",
"sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205",
"sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84",
"sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848",
"sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b",
"sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da",
"sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb",
"sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5",
"sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06",
"sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf",
"sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d",
"sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234",
"sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a",
"sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729",
"sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1",
"sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b",
"sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64",
"sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37",
"sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5",
"sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9",
"sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd",
"sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70",
"sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8",
"sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0",
"sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6",
"sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447",
"sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec",
"sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2"
],
"index": "pypi",
"version": "==1.3.23"
},
"sqlalchemy-aio": {
"hashes": [
"sha256:7f77366f55d34891c87386dd0962a28b948b684e8ea5edb7daae4187c0b291bf",
"sha256:f767320427c22c66fa5840a1f17f3261110a8ddc8560558f4fbf12d31a66b17b"
],
"index": "pypi",
"version": "==0.16.0"
},
"sse-starlette": {
"hashes": [
"sha256:1c0cc62cc7d021a386dc06a16a9ddc3e2861d19da6bc2e654e65cc111e820456"
],
"index": "pypi",
"version": "==0.6.2"
},
"starlette": {
"hashes": [
"sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed",
"sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa"
],
"markers": "python_version >= '3.6'",
"version": "==0.14.2"
},
"typing-extensions": {
"hashes": [
"sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc",
"sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3",
"sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c"
],
"index": "pypi",
"version": "==3.10.0.1"
},
"uvicorn": {
"extras": [
"standard"
],
"hashes": [
"sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1",
"sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"
],
"index": "pypi",
"version": "==0.15.0"
},
"uvloop": {
"hashes": [
"sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450",
"sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897",
"sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861",
"sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c",
"sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805",
"sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d",
"sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464",
"sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f",
"sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9",
"sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab",
"sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f",
"sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638",
"sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64",
"sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee",
"sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382",
"sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"
],
"version": "==0.16.0"
},
"watchgod": {
"hashes": [
"sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29",
"sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"
],
"version": "==0.7"
},
"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"
],
"version": "==9.1"
},
"zipp": {
"hashes": [
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
],
"markers": "python_version >= '3.6'",
"version": "==3.5.0"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==1.4.4"
},
"attrs": {
"hashes": [
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"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": [
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
],
"index": "pypi",
"version": "==20.8b1"
},
"click": {
"hashes": [
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.1"
},
"coverage": {
"hashes": [
"sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
"sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
"sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
"sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
"sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
"sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
"sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
"sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
"sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
"sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
"sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
"sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
"sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
"sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
"sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
"sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
"sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
"sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
"sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
"sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
"sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
"sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
"sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
"sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
"sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
"sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
"sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
"sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
"sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
"sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
"sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
"sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
"sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
"sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
"sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
"sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
"sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
"sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
"sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
"sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
"sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
"sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
"sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
"sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
"sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
"sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
"sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
"sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
"sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
"sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
"sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
"sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.5"
},
"importlib-metadata": {
"hashes": [
"sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
"sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
],
"markers": "python_version < '3.8'",
"version": "==4.8.1"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
],
"version": "==1.1.1"
},
"mypy": {
"hashes": [
"sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9",
"sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a",
"sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9",
"sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e",
"sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2",
"sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212",
"sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b",
"sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885",
"sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150",
"sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703",
"sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072",
"sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457",
"sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e",
"sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0",
"sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb",
"sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97",
"sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8",
"sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811",
"sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6",
"sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de",
"sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504",
"sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921",
"sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"
],
"index": "pypi",
"version": "==0.910"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"packaging": {
"hashes": [
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
],
"markers": "python_version >= '3.6'",
"version": "==21.0"
},
"pathspec": {
"hashes": [
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
],
"version": "==0.9.0"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
],
"index": "pypi",
"version": "==6.2.4"
},
"pytest-cov": {
"hashes": [
"sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a",
"sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"
],
"index": "pypi",
"version": "==2.12.1"
},
"regex": {
"hashes": [
"sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468",
"sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354",
"sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308",
"sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d",
"sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc",
"sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8",
"sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797",
"sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2",
"sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13",
"sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d",
"sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a",
"sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0",
"sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73",
"sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1",
"sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed",
"sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a",
"sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b",
"sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f",
"sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256",
"sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb",
"sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2",
"sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983",
"sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb",
"sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645",
"sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8",
"sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a",
"sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906",
"sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f",
"sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c",
"sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892",
"sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0",
"sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e",
"sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e",
"sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed",
"sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c",
"sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374",
"sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd",
"sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791",
"sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a",
"sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1",
"sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"
],
"version": "==2021.8.28"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"typed-ast": {
"hashes": [
"sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace",
"sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff",
"sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266",
"sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528",
"sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6",
"sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808",
"sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4",
"sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363",
"sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341",
"sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04",
"sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41",
"sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e",
"sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3",
"sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899",
"sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805",
"sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c",
"sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c",
"sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39",
"sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a",
"sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3",
"sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7",
"sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f",
"sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075",
"sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0",
"sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40",
"sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428",
"sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927",
"sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3",
"sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f",
"sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"
],
"markers": "python_version < '3.8'",
"version": "==1.4.3"
},
"typing-extensions": {
"hashes": [
"sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc",
"sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3",
"sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c"
],
"index": "pypi",
"version": "==3.10.0.1"
},
"zipp": {
"hashes": [
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
],
"markers": "python_version >= '3.6'",
"version": "==3.5.0"
}
}
}

View File

@ -1,28 +1,21 @@
from hypercorn.trio import serve import asyncio
import trio
import trio_asyncio
from hypercorn.config import Config
from .commands import migrate_databases, transpile_scss, bundle_vendored import uvloop
from starlette.requests import Request
trio.run(migrate_databases) from .commands import bundle_vendored, migrate_databases, transpile_scss
from .settings import (DEBUG, LNBITS_COMMIT, LNBITS_DATA_FOLDER,
LNBITS_SITE_TITLE, PORT, SERVICE_FEE, WALLET)
uvloop.install()
asyncio.create_task(migrate_databases())
transpile_scss() transpile_scss()
bundle_vendored() bundle_vendored()
from .app import create_app from .app import create_app
app = trio.run(create_app) app = create_app()
from .settings import (
LNBITS_SITE_TITLE,
SERVICE_FEE,
DEBUG,
LNBITS_DATA_FOLDER,
WALLET,
LNBITS_COMMIT,
HOST,
PORT
)
print( print(
f"""Starting LNbits with f"""Starting LNbits with
@ -35,6 +28,3 @@ print(
""" """
) )
config = Config()
config.bind = [f"{HOST}:{PORT}"]
trio_asyncio.run(serve, app, config)

View File

@ -1,38 +1,30 @@
import jinja2 import asyncio
from lnbits.jinja2_templating import Jinja2Templates
import sys
import warnings
import importlib import importlib
from lnbits.core.tasks import register_task_listeners
import sys
import traceback import traceback
import trio import warnings
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .commands import db_migrate, handle_assets
from .core import core_app
from .helpers import (
get_valid_extensions,
get_js_vendored,
get_css_vendored,
url_for_vendored,
)
from .proxy_fix import ASGIProxyFix
from .tasks import (
webhook_handler,
invoice_listener,
run_deferred_async,
check_pending_payments,
internal_invoice_listener,
catch_everything_and_restart,
)
from .settings import WALLET
from .requestvars import g, request_global
import lnbits.settings import lnbits.settings
async def create_app(config_object="lnbits.settings") -> FastAPI: from .commands import db_migrate, handle_assets
from .core import core_app
from .core.views.generic import core_html_routes
from .helpers import (get_css_vendored, get_js_vendored, get_valid_extensions,
template_renderer, url_for_vendored)
from .requestvars import g
from .settings import WALLET
from .tasks import (catch_everything_and_restart, check_pending_payments, internal_invoice_listener,
invoice_listener, run_deferred_async, webhook_handler)
def create_app(config_object="lnbits.settings") -> FastAPI:
"""Create application factory. """Create application factory.
:param config_object: The configuration object to use. :param config_object: The configuration object to use.
""" """
@ -54,7 +46,16 @@ async def create_app(config_object="lnbits.settings") -> FastAPI:
) )
g().config = lnbits.settings g().config = lnbits.settings
g().templates = build_standard_jinja_templates() g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return template_renderer().TemplateResponse("error.html", {"request": request, "err": f"`{exc.errors()}` is not a valid UUID."})
# return HTMLResponse(
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
# )
app.add_middleware(GZipMiddleware, minimum_size=1000) app.add_middleware(GZipMiddleware, minimum_size=1000)
# app.add_middleware(ASGIProxyFix) # app.add_middleware(ASGIProxyFix)
@ -68,26 +69,6 @@ async def create_app(config_object="lnbits.settings") -> FastAPI:
return app return app
def build_standard_jinja_templates():
t = Jinja2Templates(
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
)
t.env.globals["SITE_TITLE"] = lnbits.settings.LNBITS_SITE_TITLE
t.env.globals["SITE_TAGLINE"] = lnbits.settings.LNBITS_SITE_TAGLINE
t.env.globals["SITE_DESCRIPTION"] = lnbits.settings.LNBITS_SITE_DESCRIPTION
t.env.globals["LNBITS_THEME_OPTIONS"] = lnbits.settings.LNBITS_THEME_OPTIONS
t.env.globals["LNBITS_VERSION"] = lnbits.settings.LNBITS_COMMIT
t.env.globals["EXTENSIONS"] = get_valid_extensions()
if g().config.DEBUG:
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
else:
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
return t
def check_funding_source(app: FastAPI) -> None: def check_funding_source(app: FastAPI) -> None:
@app.on_event("startup") @app.on_event("startup")
async def check_wallet_status(): async def check_wallet_status():
@ -106,8 +87,9 @@ def check_funding_source(app: FastAPI) -> None:
def register_routes(app: FastAPI) -> None: def register_routes(app: FastAPI) -> None:
"""Register Flask blueprints / LNbits extensions.""" """Register FastAPI routes / LNbits extensions."""
app.include_router(core_app) app.include_router(core_app)
app.include_router(core_html_routes)
for ext in get_valid_extensions(): for ext in get_valid_extensions():
try: try:
@ -147,36 +129,23 @@ def register_async_tasks(app):
@app.on_event("startup") @app.on_event("startup")
async def listeners(): async def listeners():
run_deferred_async() loop = asyncio.get_event_loop()
trio.open_process(check_pending_payments) loop.create_task(catch_everything_and_restart(check_pending_payments))
trio.open_process(invoice_listener) loop.create_task(catch_everything_and_restart(invoice_listener))
trio.open_process(internal_invoice_listener) loop.create_task(catch_everything_and_restart(internal_invoice_listener))
await register_task_listeners()
async with trio.open_nursery() as n: await run_deferred_async()
pass
# n.start_soon(catch_everything_and_restart, check_pending_payments)
# n.start_soon(catch_everything_and_restart, invoice_listener)
# n.start_soon(catch_everything_and_restart, internal_invoice_listener)
@app.on_event("shutdown") @app.on_event("shutdown")
async def stop_listeners(): async def stop_listeners():
pass pass
def register_exception_handlers(app): def register_exception_handlers(app):
@app.errorhandler(Exception) @app.errorhandler(Exception)
async def basic_error(err): async def basic_error(request: Request, err):
print("handled error", traceback.format_exc()) print("handled error", traceback.format_exc())
etype, value, tb = sys.exc_info() etype, value, tb = sys.exc_info()
traceback.print_exception(etype, err, tb) traceback.print_exception(etype, err, tb)
exc = traceback.format_exc() exc = traceback.format_exc()
return ( return template_renderer().TemplateResponse("error.html", {"request": request, "err": err})
"\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,
)

View File

@ -1,4 +1,4 @@
import trio import asyncio
import warnings import warnings
import click import click
import importlib import importlib
@ -18,7 +18,7 @@ from .settings import LNBITS_PATH
@click.command("migrate") @click.command("migrate")
def db_migrate(): def db_migrate():
trio.run(migrate_databases) asyncio.create_task(migrate_databases())
@click.command("assets") @click.command("assets")

View File

@ -6,14 +6,8 @@ db = Database("database")
core_app: APIRouter = APIRouter() core_app: APIRouter = APIRouter()
from lnbits.tasks import record_async
from .tasks import register_listeners
from .views.api import * # noqa from .views.api import * # noqa
from .views.generic import * # noqa from .views.generic import * # noqa
from .views.public_api import * # noqa from .views.public_api import * # noqa
@core_app.on_event("startup")
def do_startup():
record_async(register_listeners)

View File

@ -54,20 +54,14 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
""", """,
(user_id,), (user_id,),
) )
else:
return None
return ( return User(
User( id = user['id'],
**{ email = user['email'],
**user, extensions = [e[0] for e in extensions],
**{ wallets = [Wallet(**w) for w in wallets])
"extensions": [e[0] for e in extensions],
"wallets": [Wallet(**w) for w in wallets],
},
}
)
if user
else None
)
async def update_user_extension( async def update_user_extension(

View File

@ -1,7 +1,7 @@
import json import json
import hmac import hmac
import hashlib import hashlib
from quart import url_for from lnbits.helpers import url_for
from ecdsa import SECP256k1, SigningKey # type: ignore from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict from typing import List, NamedTuple, Optional, Dict
@ -10,22 +10,6 @@ from pydantic import BaseModel
from lnbits.settings import WALLET from lnbits.settings import WALLET
class User(BaseModel):
id: str
email: str
extensions: List[str] = []
wallets: List["Wallet"] = []
password: Optional[str] = None
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
class Wallet(BaseModel): class Wallet(BaseModel):
id: str id: str
name: str name: str
@ -46,11 +30,12 @@ class Wallet(BaseModel):
@property @property
def lnurlwithdraw_full(self) -> str: def lnurlwithdraw_full(self) -> str:
url = url_for( url = url_for(
"core.lnurl_full_withdraw", "/withdraw",
external=True,
usr=self.user, usr=self.user,
wal=self.id, wal=self.id,
_external=True,
) )
try: try:
return lnurl_encode(url) return lnurl_encode(url)
@ -73,6 +58,22 @@ class Wallet(BaseModel):
return await get_wallet_payment(self.id, payment_hash) return await get_wallet_payment(self.id, payment_hash)
class User(BaseModel):
id: str
email: Optional[str] = None
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
class Payment(BaseModel): class Payment(BaseModel):
checking_id: str checking_id: str
pending: bool pending: bool
@ -83,10 +84,10 @@ class Payment(BaseModel):
bolt11: str bolt11: str
preimage: str preimage: str
payment_hash: str payment_hash: str
extra: Dict extra: Optional[Dict] = {}
wallet_id: str wallet_id: str
webhook: str webhook: Optional[str]
webhook_status: int webhook_status: Optional[int]
@classmethod @classmethod
def from_row(cls, row: Row): def from_row(cls, row: Row):

View File

@ -1,11 +1,10 @@
import trio import asyncio
import json import json
import httpx import httpx
from io import BytesIO from io import BytesIO
from binascii import unhexlify from binascii import unhexlify
from typing import Optional, Tuple, Dict from typing import Optional, Tuple, Dict
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
from quart import g, url_for
from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore
try: try:
@ -15,9 +14,10 @@ except ImportError: # pragma: nocover
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import Connection from lnbits.db import Connection
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus, PaymentResponse from lnbits.wallets.base import PaymentStatus, PaymentResponse
from lnbits.requestvars import g
from . import db from . import db
from .crud import ( from .crud import (
@ -211,7 +211,7 @@ async def redeem_lnurl_withdraw(
return None return None
if wait_seconds: if wait_seconds:
await trio.sleep(wait_seconds) await asyncio.sleep(wait_seconds)
params = { params = {
"k1": res["k1"], "k1": res["k1"],
@ -220,10 +220,9 @@ async def redeem_lnurl_withdraw(
try: try:
params["balanceNotify"] = url_for( params["balanceNotify"] = url_for(
"core.lnurl_balance_notify", f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
service=urlparse(lnurl_request).netloc, external=True,
wal=wallet_id, wal=wallet_id,
_external=True,
) )
except Exception: except Exception:
pass pass
@ -242,7 +241,7 @@ async def perform_lnurlauth(
cb = urlparse(callback) cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0]) k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g.wallet.lnurlauth_key(cb.netloc) key = g().wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes: def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks""" """for strict DER we need to encode the integer with some quirks"""

View File

@ -1,4 +1,4 @@
import trio import asyncio
import httpx import httpx
from typing import List from typing import List
@ -8,17 +8,19 @@ from . import db
from .crud import get_balance_notify from .crud import get_balance_notify
from .models import Payment from .models import Payment
api_invoice_listeners: List[trio.MemorySendChannel] = [] api_invoice_listeners: List[asyncio.Queue] = []
async def register_listeners(): async def register_task_listeners():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5) invoice_paid_queue = asyncio.Queue(5)
register_invoice_listener(invoice_paid_chan_send) register_invoice_listener(invoice_paid_queue)
await wait_for_paid_invoices(invoice_paid_chan_recv) asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
async for payment in invoice_paid_chan: while True:
payment = await invoice_paid_queue.get()
# send information to sse channel # send information to sse channel
await dispatch_invoice_listener(payment) await dispatch_invoice_listener(payment)
@ -43,8 +45,8 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async def dispatch_invoice_listener(payment: Payment): async def dispatch_invoice_listener(payment: Payment):
for send_channel in api_invoice_listeners: for send_channel in api_invoice_listeners:
try: try:
send_channel.send_nowait(payment) send_channel.put_nowait(payment)
except trio.WouldBlock: except asyncio.QueueFull:
print("removing sse listener", send_channel) print("removing sse listener", send_channel)
api_invoice_listeners.remove(send_channel) api_invoice_listeners.remove(send_channel)

View File

@ -1,69 +1,60 @@
from fastapi.param_functions import Depends import asyncio
from lnbits.auth_bearer import AuthBearer
from pydantic import BaseModel
import trio
import json
import httpx
import hashlib import hashlib
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult import json
from quart import current_app, make_response, url_for
from fastapi import Query
from http import HTTPStatus
from binascii import unhexlify from binascii import unhexlify
from typing import Dict, List, Optional, Union from http import HTTPStatus
from typing import Dict, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx
from fastapi import Query, Request
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
from sse_starlette.sse import EventSourceResponse
from pydantic import BaseModel
from lnbits import bolt11, lnurl from lnbits import bolt11, lnurl
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.core.models import Payment, Wallet
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker,
WalletTypeInfo, get_key_type)
from lnbits.helpers import url_for
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
from .. import core_app, db from .. import core_app, db
from ..crud import get_payments, save_balance_check, update_wallet from ..crud import get_payments, save_balance_check, update_wallet
from ..services import ( from ..services import (InvoiceFailure, PaymentFailure, create_invoice,
PaymentFailure, pay_invoice, perform_lnurlauth)
InvoiceFailure,
create_invoice,
pay_invoice,
perform_lnurlauth,
)
from ..tasks import api_invoice_listeners from ..tasks import api_invoice_listeners
@core_app.get( @core_app.get("/api/v1/wallet")
"/api/v1/wallet", async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
# dependencies=[Depends(AuthBearer())]
)
# @api_check_wallet_key("invoice")
async def api_wallet():
return ( return (
{"id": g().wallet.id, "name": g().wallet.name, "balance": g().wallet.balance_msat}, {"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat},
HTTPStatus.OK, HTTPStatus.OK,
) )
@core_app.put("/api/v1/wallet/{new_name}") @core_app.put("/api/v1/wallet/{new_name}")
@api_check_wallet_key("invoice") async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_update_wallet(new_name: str): await update_wallet(wallet.wallet.id, new_name)
await update_wallet(g().wallet.id, new_name)
return ( return (
{ {
"id": g().wallet.id, "id": wallet.wallet.id,
"name": g().wallet.name, "name": wallet.wallet.name,
"balance": g().wallet.balance_msat, "balance": wallet.wallet.balance_msat,
}, },
HTTPStatus.OK, HTTPStatus.OK,
) )
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
@api_check_wallet_key("invoice") async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_payments(): return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
return (
await get_payments(wallet_id=g().wallet.id, pending=True, complete=True),
HTTPStatus.OK,
)
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
amount: int = Query(None, ge=1) amount: int = Query(None, ge=1)
@ -75,9 +66,7 @@ class CreateInvoiceData(BaseModel):
extra: Optional[dict] = None extra: Optional[dict] = None
webhook: Optional[str] = None webhook: Optional[str] = None
@api_check_wallet_key("invoice") async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
async def api_payments_create_invoice(data: CreateInvoiceData):
if "description_hash" in data: if "description_hash" in data:
description_hash = unhexlify(data.description_hash) description_hash = unhexlify(data.description_hash)
memo = "" memo = ""
@ -94,7 +83,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
async with db.connect() as conn: async with db.connect() as conn:
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=g().wallet.id, wallet_id=wallet.id,
amount=amount, amount=amount,
memo=memo, memo=memo,
description_hash=description_hash, description_hash=description_hash,
@ -121,10 +110,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
params={ params={
"pr": payment_request, "pr": payment_request,
"balanceNotify": url_for( "balanceNotify": url_for(
"core.lnurl_balance_notify", f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
service=urlparse(data.lnurl_callback).netloc, external=True,
wal=g().wallet.id, wal=g().wallet.id,
_external=True,
), ),
}, },
timeout=10, timeout=10,
@ -152,10 +140,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
) )
@api_check_wallet_key("admin") async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
async def api_payments_pay_invoice(
bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None)
):
try: try:
payment_hash = await pay_invoice( payment_hash = await pay_invoice(
wallet_id=wallet.id, wallet_id=wallet.id,
@ -180,11 +165,20 @@ async def api_payments_pay_invoice(
) )
@core_app.post("/api/v1/payments") @core_app.post("/api/v1/payments", deprecated=True,
async def api_payments_create(out: bool = True): description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead")
if out is True: async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True,
return await api_payments_pay_invoice() invoiceData: Optional[CreateInvoiceData] = Body(None),
return await api_payments_create_invoice() bolt11: Optional[str] = Query(None)):
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:
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_create_invoice(invoiceData, wallet.wallet) # invoice key
class CreateLNURLData(BaseModel): class CreateLNURLData(BaseModel):
description_hash: str description_hash: str
@ -193,8 +187,7 @@ class CreateLNURLData(BaseModel):
comment: Optional[str] = None comment: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
@core_app.post("/api/v1/payments/lnurl") @core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())])
@api_check_wallet_key("admin")
async def api_payments_pay_lnurl(data: CreateLNURLData): async def api_payments_pay_lnurl(data: CreateLNURLData):
domain = urlparse(data.callback).netloc domain = urlparse(data.callback).netloc
@ -259,11 +252,46 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
HTTPStatus.CREATED, HTTPStatus.CREATED,
) )
async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id
payment_queue = asyncio.Queue(0)
print("adding sse listener", payment_queue)
api_invoice_listeners.append(payment_queue)
send_queue = asyncio.Queue(0)
async def payment_received() -> None:
while True:
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
await send_queue.put(("payment-received", payment))
asyncio.create_task(payment_received())
try:
while True:
typ, data = await send_queue.get()
message = [f"event: {typ}".encode("utf-8")]
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
message.append(f"data: {jdata}".encode("utf-8"))
yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8"))
except asyncio.CancelledError:
return
@core_app.get("/api/v1/payments/sse")
async def api_payments_sse(request: Request, wallet: WalletTypeInfo = Depends(get_key_type)):
return EventSourceResponse(subscribe(request, wallet))
@core_app.get("/api/v1/payments/{payment_hash}") @core_app.get("/api/v1/payments/{payment_hash}")
@api_check_wallet_key("invoice") async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_payment(payment_hash): payment = await wallet.wallet.get_payment(payment_hash)
payment = await g().wallet.get_payment(payment_hash)
if not payment: if not payment:
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
@ -280,62 +308,7 @@ async def api_payment(payment_hash):
HTTPStatus.OK, HTTPStatus.OK,
) )
@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())])
@core_app.get("/api/v1/payments/sse")
@api_check_wallet_key("invoice", accept_querystring=True)
async def api_payments_sse():
this_wallet_id = g().wallet.id
send_payment, receive_payment = trio.open_memory_channel(0)
print("adding sse listener", send_payment)
api_invoice_listeners.append(send_payment)
send_event, event_to_send = trio.open_memory_channel(0)
async def payment_received() -> None:
async for payment in receive_payment:
if payment.wallet_id == this_wallet_id:
await send_event.send(("payment-received", payment))
async def repeat_keepalive():
await trio.sleep(1)
while True:
await send_event.send(("keepalive", ""))
await trio.sleep(25)
current_app.nursery.start_soon(payment_received)
current_app.nursery.start_soon(repeat_keepalive)
async def send_events():
try:
async for typ, data in event_to_send:
message = [f"event: {typ}".encode("utf-8")]
if data:
jdata = json.dumps(dict(data._asdict(), pending=False))
message.append(f"data: {jdata}".encode("utf-8"))
yield b"\n".join(message) + b"\r\n\r\n"
except trio.Cancelled:
return
response = await make_response(
send_events(),
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
"Transfer-Encoding": "chunked",
},
)
response.timeout = None
return response
@core_app.get("/api/v1/lnurlscan/{code}")
@api_check_wallet_key("invoice")
async def api_lnurlscan(code: str): async def api_lnurlscan(code: str):
try: try:
url = lnurl.decode(code) url = lnurl.decode(code)
@ -444,8 +417,7 @@ async def api_lnurlscan(code: str):
return params return params
@core_app.post("/api/v1/lnurlauth") @core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
@api_check_wallet_key("admin")
async def api_perform_lnurlauth(callback: str): async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback) err = await perform_lnurlauth(callback)
if err: if err:
@ -453,6 +425,6 @@ async def api_perform_lnurlauth(callback: str):
return "", HTTPStatus.OK return "", HTTPStatus.OK
@core_app.route("/api/v1/currencies", methods=["GET"]) @core_app.get("/api/v1/currencies")
async def api_list_currencies_available(): async def api_list_currencies_available():
return list(currencies.keys()) return list(currencies.keys())

View File

@ -1,51 +1,49 @@
from lnbits.requestvars import g import asyncio
from os import path
from http import HTTPStatus from http import HTTPStatus
from typing import Optional from typing import Optional
import jinja2
from fastapi import Request, status
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Body
from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter
from pydantic.types import UUID4
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from lnbits.core import core_app, db from lnbits.core import db
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.helpers import template_renderer, url_for
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_SITE_TITLE from lnbits.requestvars import g
from lnbits.settings import (LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE,
SERVICE_FEE)
from ..crud import ( from ..crud import (create_account, create_wallet, delete_wallet,
create_account, get_balance_check, get_user, save_balance_notify,
get_user, update_user_extension)
update_user_extension, from ..services import pay_invoice, redeem_lnurl_withdraw
create_wallet,
delete_wallet,
get_balance_check,
save_balance_notify,
)
from ..services import redeem_lnurl_withdraw, pay_invoice
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
from lnbits.jinja2_templating import Jinja2Templates
core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"])
@core_app.get("/favicon.ico") @core_html_routes.get("/favicon.ico")
async def favicon(): async def favicon():
return FileResponse("lnbits/core/static/favicon.ico") return FileResponse("lnbits/core/static/favicon.ico")
@core_app.get("/", response_class=HTMLResponse) @core_html_routes.get("/", response_class=HTMLResponse)
async def home(request: Request, lightning: str = None): async def home(request: Request, lightning: str = None):
return g().templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning}) return template_renderer().TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
@core_app.get("/extensions") @core_html_routes.get("/extensions")
@validate_uuids(["usr"], required=True) # @validate_uuids(["usr"], required=True)
@check_user_exists() # @check_user_exists()
async def extensions(enable: str, disable: str): async def extensions(request: Request, enable: str, disable: str):
extension_to_enable = enable extension_to_enable = enable
extension_to_disable = disable extension_to_disable = disable
if extension_to_enable and extension_to_disable: if extension_to_enable and extension_to_disable:
abort( raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
if extension_to_enable: if extension_to_enable:
await update_user_extension( await update_user_extension(
@ -55,15 +53,16 @@ async def extensions(enable: str, disable: str):
await update_user_extension( await update_user_extension(
user_id=g.user.id, extension=extension_to_disable, active=False user_id=g.user.id, extension=extension_to_disable, active=False
) )
return await templates.TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)}) return template_renderer().TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)})
@core_app.get("/wallet{usr}{wal}{nme}") @core_html_routes.get("/wallet", response_class=HTMLResponse)
#Not sure how to validate #Not sure how to validate
@validate_uuids(["usr", "wal"]) # @validate_uuids(["usr", "nme"])
async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme: Optional[str]): async def wallet(request: Request = Query(None), nme: Optional[str] = Query(None),
user_id = usr usr: Optional[UUID4] = Query(None), wal: Optional[UUID4] = Query(None)):
wallet_id = wal user_id = usr.hex if usr else None
wallet_id = wal.hex if wal else None
wallet_name = nme wallet_name = nme
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
@ -78,32 +77,29 @@ async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme:
else: else:
user = await get_user(user_id) user = await get_user(user_id)
if not user: if not user:
abort(HTTPStatus.NOT_FOUND, "User does not exist.") return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User does not exist."})
return
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User not authorized."})
if not wallet_id: if not wallet_id:
if user.wallets and not wallet_name: if user.wallets and not wallet_name:
wallet = user.wallets[0] wallet = user.wallets[0]
else: else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
wallet = user.get_wallet(wallet_id) wallet = user.get_wallet(wallet_id)
if not wallet: if not wallet:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.") return template_renderer().TemplateResponse("error.html", {"request": request, "err": "Wallet not found"})
return await templates.TemplateResponse( return template_renderer().TemplateResponse(
"core/wallet.html", {"request":request,"user":user, "wallet":wallet, "service_fee":service_fee} "core/wallet.html", {"request":request,"user":user.dict(), "wallet":wallet.dict(), "service_fee":service_fee}
) )
@core_app.get("/withdraw") @core_html_routes.get("/withdraw")
@validate_uuids(["usr", "wal"], required=True) # @validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw(): async def lnurl_full_withdraw(request: Request):
user = await get_user(request.args.get("usr")) user = await get_user(request.args.get("usr"))
if not user: if not user:
return {"status": "ERROR", "reason": "User does not exist."} return {"status": "ERROR", "reason": "User does not exist."}
@ -115,24 +111,22 @@ async def lnurl_full_withdraw():
return { return {
"tag": "withdrawRequest", "tag": "withdrawRequest",
"callback": url_for( "callback": url_for(
"core.lnurl_full_withdraw_callback", "/withdraw/cb",
external=True,
usr=user.id, usr=user.id,
wal=wallet.id, wal=wallet.id,
_external=True,
), ),
"k1": "0", "k1": "0",
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0, "minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance, "maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}", "defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
"balanceCheck": url_for( "balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
"core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True
),
} }
@core_app.get("/withdraw/cb") @core_html_routes.get("/withdraw/cb")
@validate_uuids(["usr", "wal"], required=True) # @validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw_callback(): async def lnurl_full_withdraw_callback(request: Request):
user = await get_user(request.args.get("usr")) user = await get_user(request.args.get("usr"))
if not user: if not user:
return {"status": "ERROR", "reason": "User does not exist."} return {"status": "ERROR", "reason": "User does not exist."}
@ -149,7 +143,7 @@ async def lnurl_full_withdraw_callback():
except: except:
pass pass
current_app.nursery.start_soon(pay) asyncio.create_task(pay())
balance_notify = request.args.get("balanceNotify") balance_notify = request.args.get("balanceNotify")
if balance_notify: if balance_notify:
@ -158,53 +152,55 @@ async def lnurl_full_withdraw_callback():
return {"status": "OK"} return {"status": "OK"}
@core_app.get("/deletewallet") @core_html_routes.get("/deletewallet")
@validate_uuids(["usr", "wal"], required=True) # @validate_uuids(["usr", "wal"], required=True)
@check_user_exists() # @check_user_exists()
async def deletewallet(): async def deletewallet(request: Request):
wallet_id = request.args.get("wal", type=str) wallet_id = request.path_params.get("wal", type=str)
user_wallet_ids = g.user.wallet_ids user_wallet_ids = g().user.wallet_ids
if wallet_id not in user_wallet_ids: if wallet_id not in user_wallet_ids:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.") raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
else: else:
await delete_wallet(user_id=g.user.id, wallet_id=wallet_id) await delete_wallet(user_id=g().user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id) user_wallet_ids.remove(wallet_id)
if user_wallet_ids: if user_wallet_ids:
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0])) return RedirectResponse(url_for("/wallet", usr=g().user.id, wal=user_wallet_ids[0]),
status_code=status.HTTP_307_TEMPORARY_REDIRECT)
return redirect(url_for("core.home")) return RedirectResponse(url_for("/"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@core_app.get("/withdraw/notify/{service}") @core_html_routes.get("/withdraw/notify/{service}")
@validate_uuids(["wal"], required=True) # @validate_uuids(["wal"], required=True)
async def lnurl_balance_notify(service: str): async def lnurl_balance_notify(request: Request, service: str):
bc = await get_balance_check(request.args.get("wal"), service) bc = await get_balance_check(request.args.get("wal"), service)
if bc: if bc:
redeem_lnurl_withdraw(bc.wallet, bc.url) redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_app.get("/lnurlwallet") @core_html_routes.get("/lnurlwallet")
async def lnurlwallet(): async def lnurlwallet(request: Request):
async with db.connect() as conn: async with db.connect() as conn:
account = await create_account(conn=conn) account = await create_account(conn=conn)
user = await get_user(account.id, conn=conn) user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn)
current_app.nursery.start_soon( asyncio.create_task(
redeem_lnurl_withdraw, redeem_lnurl_withdraw(
wallet.id, wallet.id,
request.args.get("lightning"), request.args.get("lightning"),
"LNbits initial funding: voucher redeem.", "LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"}, {"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service 5 # wait 5 seconds before sending the invoice to the service
)
) )
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@core_app.get("/manifest/{usr}.webmanifest") @core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str): async def manifest(usr: str):
user = await get_user(usr) user = await get_user(usr)
if not user: if not user:

View File

@ -1,7 +1,6 @@
import trio import asyncio
import datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from quart import jsonify
from lnbits import bolt11 from lnbits import bolt11
@ -27,27 +26,27 @@ async def api_public_payment_longpolling(payment_hash):
except: except:
return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST
send_payment, receive_payment = trio.open_memory_channel(0) payment_queue = asyncio.Queue(0)
print("adding standalone invoice listener", payment_hash, send_payment) print("adding standalone invoice listener", payment_hash, payment_queue)
api_invoice_listeners.append(send_payment) api_invoice_listeners.append(payment_queue)
response = None response = None
async def payment_info_receiver(cancel_scope): async def payment_info_receiver(cancel_scope):
async for payment in receive_payment: async for payment in payment_queue.get():
if payment.payment_hash == payment_hash: if payment.payment_hash == payment_hash:
nonlocal response nonlocal response
response = ({"status": "paid"}, HTTPStatus.OK) response = ({"status": "paid"}, HTTPStatus.OK)
cancel_scope.cancel() cancel_scope.cancel()
async def timeouter(cancel_scope): async def timeouter(cancel_scope):
await trio.sleep(45) await asyncio.sleep(45)
cancel_scope.cancel() cancel_scope.cancel()
async with trio.open_nursery() as nursery:
nursery.start_soon(payment_info_receiver, nursery.cancel_scope) asyncio.create_task(payment_info_receiver())
nursery.start_soon(timeouter, nursery.cancel_scope) asyncio.create_task(timeouter())
if response: if response:
return response return response

View File

@ -1,12 +1,12 @@
import os import os
import trio import asyncio
import time import time
import datetime import datetime
from typing import Optional from typing import Optional
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from sqlalchemy import create_engine # type: ignore from sqlalchemy import create_engine
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.base import AsyncConnection # type: ignore from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL
@ -132,8 +132,8 @@ class Database(Compat):
else: else:
self.schema = None self.schema = None
self.engine = create_engine(database_uri, strategy=TRIO_STRATEGY) self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
self.lock = trio.StrictFIFOLock() self.lock = asyncio.Lock()
@asynccontextmanager @asynccontextmanager
async def connect(self): async def connect(self):

View File

@ -1,36 +1,114 @@
from cerberus import Validator # type: ignore
from quart import g, abort, jsonify, request
from functools import wraps from functools import wraps
from http import HTTPStatus from http import HTTPStatus
from fastapi.security import api_key
from lnbits.core.models import Wallet
from typing import List, Union from typing import List, Union
from uuid import UUID from uuid import UUID
from cerberus import Validator # type: ignore
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.params import Security
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.settings import LNBITS_ALLOWED_USERS
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import LNBITS_ALLOWED_USERS
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
try:
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
g().wallet = await get_wallet_for_key(key_value, key_type)
except KeyError:
return (
jsonify({"message": "`X-Api-Key` header missing."}),
HTTPStatus.BAD_REQUEST,
)
if not g().wallet: class KeyChecker(SecurityBase):
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
self._key_type = "invoice"
self._api_key = api_key
if api_key:
self.model: APIKey= APIKey(
**{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY"
)
else:
self.model: APIKey= APIKey(
**{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER"
)
self.wallet = None
return await view(**kwargs) async def __call__(self, request: Request) -> Wallet:
try:
key_value = self._api_key if self._api_key else request.headers.get("X-API-KEY") or request.query_params["api-key"]
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
# Also, we should not return the wallet here - thats silly.
# Possibly store it in a Redis DB
self.wallet = await get_wallet_for_key(key_value, self._key_type)
if not self.wallet:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid key or expired key.")
return wrapped_view except KeyError:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST,
detail="`X-API-KEY` header missing.")
return wrap class WalletInvoiceKeyChecker(KeyChecker):
"""
WalletInvoiceKeyChecker will ensure that the provided invoice
wallet key is correct and populate g().wallet with the wallet
for the key in `X-API-key`.
The checker will raise an HTTPException when the key is wrong in some ways.
"""
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "invoice"
class WalletAdminKeyChecker(KeyChecker):
"""
WalletAdminKeyChecker will ensure that the provided admin
wallet key is correct and populate g().wallet with the wallet
for the key in `X-API-key`.
The checker will raise an HTTPException when the key is wrong in some ways.
"""
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "admin"
class WalletTypeInfo():
wallet_type: int
wallet: Wallet
def __init__(self, wallet_type: int, wallet: Wallet) -> None:
self.wallet_type = wallet_type
self.wallet = wallet
api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's")
api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's")
async def get_key_type(r: Request,
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query)) -> WalletTypeInfo:
# 0: admin
# 1: invoice
# 2: invalid
try:
checker = WalletAdminKeyChecker(api_key=api_key_query)
await checker.__call__(r)
return WalletTypeInfo(0, checker.wallet)
except HTTPException as e:
if e.status_code == HTTPStatus.UNAUTHORIZED:
pass
except:
raise
try:
checker = WalletInvoiceKeyChecker()
await checker.__call__(r)
return WalletTypeInfo(1, checker.wallet)
except HTTPException as e:
if e.status_code == HTTPStatus.UNAUTHORIZED:
return WalletTypeInfo(2, None)
except:
raise
def api_validate_post_request(*, schema: dict): def api_validate_post_request(*, schema: dict):
def wrap(view): def wrap(view):
@ -77,28 +155,4 @@ def check_user_exists(param: str = "usr"):
return wrap return wrap
def validate_uuids(
params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4
):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
query_params = {
param: request.args.get(param, type=str) for param in params
}
for param, value in query_params.items():
if not value and (required is True or (required and param in required)):
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
if value:
try:
UUID(value, version=version)
except ValueError:
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
return await view(**kwargs)
return wrapped_view
return wrap

View File

@ -1,5 +1,4 @@
import hashlib import hashlib
from quart import jsonify, url_for, request
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice

View File

@ -2,7 +2,7 @@ import json
import base64 import base64
import hashlib import hashlib
from collections import OrderedDict from collections import OrderedDict
from quart import url_for
from typing import Optional, List, Dict from typing import Optional, List, Dict
from lnurl import encode as lnurl_encode # type: ignore from lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore

View File

@ -1,9 +1,8 @@
import time import time
from datetime import datetime from datetime import datetime
from quart import g, render_template, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.crud import get_standalone_payment from lnbits.core.crud import get_standalone_payment
@ -15,8 +14,8 @@ from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@offlineshop_ext.get("/") @offlineshop_ext.get("/")
@validate_uuids(["usr"], required=True) # @validate_uuids(["usr"], required=True)
@check_user_exists() # @check_user_exists()
async def index(request: Request): async def index(request: Request):
return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user}) return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user})

View File

@ -1,11 +1,12 @@
from typing import Optional from typing import Optional
from pydantic.main import BaseModel from pydantic.main import BaseModel
from quart import g, jsonify
from http import HTTPStatus from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.utils.exchange_rates import currencies from lnbits.utils.exchange_rates import currencies
from lnbits.requestvars import g
from . import offlineshop_ext from . import offlineshop_ext
from .crud import ( from .crud import (
@ -27,7 +28,7 @@ async def api_list_currencies_available():
@offlineshop_ext.get("/api/v1/offlineshop") @offlineshop_ext.get("/api/v1/offlineshop")
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_shop_from_wallet(): async def api_shop_from_wallet():
shop = await get_or_create_shop_by_wallet(g.wallet.id) shop = await get_or_create_shop_by_wallet(g().wallet.id)
items = await get_items(shop.id) items = await get_items(shop.id)
try: try:
@ -60,7 +61,7 @@ class CreateItemsData(BaseModel):
@offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}") @offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}")
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_add_or_update_item(data: CreateItemsData, item_id=None): async def api_add_or_update_item(data: CreateItemsData, item_id=None):
shop = await get_or_create_shop_by_wallet(g.wallet.id) shop = await get_or_create_shop_by_wallet(g().wallet.id)
if item_id == None: if item_id == None:
await add_item( await add_item(
shop.id, shop.id,
@ -87,7 +88,7 @@ async def api_add_or_update_item(data: CreateItemsData, item_id=None):
@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}") @offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}")
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_delete_item(item_id): async def api_delete_item(item_id):
shop = await get_or_create_shop_by_wallet(g.wallet.id) shop = await get_or_create_shop_by_wallet(g().wallet.id)
await delete_item_from_shop(shop.id, item_id) await delete_item_from_shop(shop.id, item_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -104,7 +105,7 @@ async def api_set_method(data: CreateMethodData):
wordlist = data.wordlist.split("\n") if data.wordlist else None wordlist = data.wordlist.split("\n") if data.wordlist else None
wordlist = [word.strip() for word in wordlist if word.strip()] wordlist = [word.strip() for word in wordlist if word.strip()]
shop = await get_or_create_shop_by_wallet(g.wallet.id) shop = await get_or_create_shop_by_wallet(g().wallet.id)
if not shop: if not shop:
return "", HTTPStatus.NOT_FOUND return "", HTTPStatus.NOT_FOUND

View File

@ -6,31 +6,34 @@ from lnbits.decorators import check_user_exists, validate_uuids
from . import withdraw_ext from . import withdraw_ext
from .crud import get_withdraw_link, chunks from .crud import get_withdraw_link, chunks
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, Response
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@withdraw_ext.get("/") @withdraw_ext.get("/", status_code=HTTPStatus.OK)
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
async def index(request: Request): async def index(request: Request):
return await templates.TemplateResponse("withdraw/index.html", {"request":request,"user":g.user}) return await templates.TemplateResponse("withdraw/index.html", {"request":request,"user":g.user})
@withdraw_ext.get("/<link_id>") @withdraw_ext.get("/{link_id}", status_code=HTTPStatus.OK)
async def display(request: Request, link_id): async def display(request: Request, link_id, response: Response):
link = await get_withdraw_link(link_id, 0) or abort( link = await get_withdraw_link(link_id, 0)
HTTPStatus.NOT_FOUND, "Withdraw link does not exist." if not link:
) response.status_code = HTTPStatus.NOT_FOUND
return "Withdraw link does not exist." #probably here is where we should return the 404??
return await templates.TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True}) return await templates.TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True})
@withdraw_ext.get("/img/<link_id>") @withdraw_ext.get("/img/{link_id}", status_code=HTTPStatus.OK)
async def img(request: Request, link_id): async def img(request: Request, link_id, response: Response):
link = await get_withdraw_link(link_id, 0) or abort( link = await get_withdraw_link(link_id, 0)
HTTPStatus.NOT_FOUND, "Withdraw link does not exist." if not link:
) response.status_code = HTTPStatus.NOT_FOUND
return "Withdraw link does not exist."
qr = pyqrcode.create(link.lnurl) qr = pyqrcode.create(link.lnurl)
stream = BytesIO() stream = BytesIO()
qr.svg(stream, scale=3) qr.svg(stream, scale=3)
@ -46,19 +49,21 @@ async def img(request: Request, link_id):
) )
@withdraw_ext.get("/print/<link_id>") @withdraw_ext.get("/print/{link_id}", status_code=HTTPStatus.OK)
async def print_qr(request: Request, link_id): async def print_qr(request: Request, link_id, response: Response):
link = await get_withdraw_link(link_id) or abort( link = await get_withdraw_link(link_id)
HTTPStatus.NOT_FOUND, "Withdraw link does not exist." if not link:
) response.status_code = HTTPStatus.NOT_FOUND
return "Withdraw link does not exist."
if link.uses == 0: if link.uses == 0:
return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,link:link, unique:False}) return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,link:link, unique:False})
links = [] links = []
count = 0 count = 0
for x in link.usescsv.split(","): for x in link.usescsv.split(","):
linkk = await get_withdraw_link(link_id, count) or abort( linkk = await get_withdraw_link(link_id, count)
HTTPStatus.NOT_FOUND, "Withdraw link does not exist." if not linkk:
) response.status_code = HTTPStatus.NOT_FOUND
return "Withdraw link does not exist."
links.append(str(linkk.lnurl)) links.append(str(linkk.lnurl))
count = count + 1 count = count + 1
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))

View File

@ -5,7 +5,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import FastAPI, Query from fastapi import FastAPI, Query, Response
from . import withdraw_ext from . import withdraw_ext
from .crud import ( from .crud import (
@ -19,47 +19,41 @@ from .crud import (
) )
@withdraw_ext.get("/api/v1/links") @withdraw_ext.get("/api/v1/links", status_code=200)
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_links(): async def api_links(response: Response):
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
try: try:
return ( return [
[
{ {
**link._asdict(), **link._asdict(),
**{"lnurl": link.lnurl}, **{"lnurl": link.lnurl},
} }
for link in await get_withdraw_links(wallet_ids) for link in await get_withdraw_links(wallet_ids)
], ]
HTTPStatus.OK,
)
except LnurlInvalidUrl: except LnurlInvalidUrl:
return ( response.status_code = HTTPStatus.UPGRADE_REQUIRED
{ return { "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." }
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
},
HTTPStatus.UPGRADE_REQUIRED,
)
@withdraw_ext.get("/api/v1/links/{link_id}") @withdraw_ext.get("/api/v1/links/{link_id}", status_code=200)
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_link_retrieve(link_id): async def api_link_retrieve(link_id, response: Response):
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
return ({"message": "Withdraw link does not exist."}, response.status_code = HTTPStatus.NOT_FOUND
HTTPStatus.NOT_FOUND, return {"message": "Withdraw link does not exist."}
)
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN response.status_code = HTTPStatus.FORBIDDEN
return {"message": "Not your withdraw link."}
return {**link, **{"lnurl": link.lnurl}}, HTTPStatus.OK return {**link, **{"lnurl": link.lnurl}}
class CreateData(BaseModel): class CreateData(BaseModel):
title: str = Query(...) title: str = Query(...)
@ -69,17 +63,15 @@ class CreateData(BaseModel):
wait_time: int = Query(..., ge=1) wait_time: int = Query(..., ge=1)
is_unique: bool is_unique: bool
@withdraw_ext.post("/api/v1/links") @withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@withdraw_ext.put("/api/v1/links/{link_id}") @withdraw_ext.put("/api/v1/links/{link_id}")
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
async def api_link_create_or_update(data: CreateData, link_id: str = None): async def api_link_create_or_update(data: CreateData, link_id: str = None, response: Response):
if data.max_withdrawable < data.min_withdrawable: if data.max_withdrawable < data.min_withdrawable:
return ( response.status_code = HTTPStatus.BAD_REQUEST
{ return {
"message": "`max_withdrawable` needs to be at least `min_withdrawable`." "message": "`max_withdrawable` needs to be at least `min_withdrawable`."
}, }
HTTPStatus.BAD_REQUEST,
)
usescsv = "" usescsv = ""
for i in range(data.uses): for i in range(data.uses):
@ -92,43 +84,41 @@ async def api_link_create_or_update(data: CreateData, link_id: str = None):
if link_id: if link_id:
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
return ( response.status_code = HTTPStatus.NOT_FOUND
jsonify({"message": "Withdraw link does not exist."}), return {"message": "Withdraw link does not exist."}
HTTPStatus.NOT_FOUND,
)
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN response.status_code = HTTPStatus.FORBIDDEN
return {"message": "Not your withdraw link."}
link = await update_withdraw_link(link_id, **data, usescsv=usescsv, used=0) link = await update_withdraw_link(link_id, **data, usescsv=usescsv, used=0)
else: else:
link = await create_withdraw_link( link = await create_withdraw_link(
wallet_id=g.wallet.id, **data, usescsv=usescsv wallet_id=g.wallet.id, **data, usescsv=usescsv
) )
if link_id:
return ({**link, **{"lnurl": link.lnurl}}, response.status_code = HTTPStatus.OK
HTTPStatus.OK if link_id else HTTPStatus.CREATED, return {**link, **{"lnurl": link.lnurl}}
)
@withdraw_ext.delete("/api/v1/links/{link_id}") @withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.NO_CONTENT)
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
async def api_link_delete(link_id): async def api_link_delete(link_id, response: Response):
link = await get_withdraw_link(link_id) link = await get_withdraw_link(link_id)
if not link: if not link:
return ({"message": "Withdraw link does not exist."}, response.status_code = HTTPStatus.NOT_FOUND
HTTPStatus.NOT_FOUND, return {"message": "Withdraw link does not exist."}
)
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN response.status_code = HTTPStatus.FORBIDDEN
return {"message": "Not your withdraw link."}
await delete_withdraw_link(link_id) await delete_withdraw_link(link_id)
return "", HTTPStatus.NO_CONTENT return ""
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}") @withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK)
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_hash_retrieve(the_hash, lnurl_id): async def api_hash_retrieve(the_hash, lnurl_id):
hashCheck = await get_hash_check(the_hash, lnurl_id) hashCheck = await get_hash_check(the_hash, lnurl_id)
return hashCheck, HTTPStatus.OK return hashCheck

View File

@ -1,11 +1,15 @@
import glob
import json import json
import os import os
import glob from typing import Any, List, NamedTuple, Optional
import jinja2
import shortuuid # type: ignore import shortuuid # type: ignore
from typing import List, NamedTuple, Optional from lnbits.jinja2_templating import Jinja2Templates
from lnbits.requestvars import g
from .settings import LNBITS_DISABLED_EXTENSIONS, LNBITS_PATH import lnbits.settings as settings
class Extension(NamedTuple): class Extension(NamedTuple):
@ -20,9 +24,9 @@ class Extension(NamedTuple):
class ExtensionManager: class ExtensionManager:
def __init__(self): def __init__(self):
self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
self._extension_folders: List[str] = [ self._extension_folders: List[str] = [
x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions")) x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
][0] ][0]
@property @property
@ -37,7 +41,7 @@ class ExtensionManager:
]: ]:
try: try:
with open( with open(
os.path.join(LNBITS_PATH, "extensions", extension, "config.json") os.path.join(settings.LNBITS_PATH, "extensions", extension, "config.json")
) as json_file: ) as json_file:
config = json.load(json_file) config = json.load(json_file)
is_valid = True is_valid = True
@ -105,7 +109,7 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]:
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = [] paths: List[str] = []
for path in glob.glob( for path in glob.glob(
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True os.path.join(settings.LNBITS_PATH, "static/vendor/**"), recursive=True
): ):
if path.endswith(".min" + ext): if path.endswith(".min" + ext):
# path is minified # path is minified
@ -131,4 +135,36 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
def url_for_vendored(abspath: str) -> str: def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH) return "/" + os.path.relpath(abspath, settings.LNBITS_PATH)
def url_for(
endpoint: str,
external: Optional[bool] = False,
**params: Any,
) -> str:
base = g().base_url if external else ""
url_params = "?"
for key in params:
url_params += f"{key}={params[key]}&"
url = f"{base}{endpoint}{url_params}"
return url
def template_renderer() -> Jinja2Templates:
t = Jinja2Templates(
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
)
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
t.env.globals["LNBITS_VERSION"] = settings.LNBITS_COMMIT
t.env.globals["EXTENSIONS"] = get_valid_extensions()
if settings.DEBUG:
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
else:
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
return t

View File

@ -135,7 +135,7 @@ window.LNbits = {
return obj return obj
}, },
user: function (data) { user: function (data) {
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data) var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
var mapWallet = this.wallet var mapWallet = this.wallet
obj.wallets = obj.wallets obj.wallets = obj.wallets
.map(function (obj) { .map(function (obj) {
@ -153,35 +153,30 @@ window.LNbits = {
return obj return obj
}, },
wallet: function (data) { wallet: function (data) {
var obj = _.object( newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
['id', 'name', 'user', 'adminkey', 'inkey', 'balance'], newWallet.msat = data.balance_msat
data newWallet.sat = Math.round(data.balance_msat / 1000)
) newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
obj.msat = obj.balance newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
obj.sat = Math.round(obj.balance / 1000) return newWallet
obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
return obj
}, },
payment: function (data) { payment: function (data) {
var obj = _.object( obj = {
[ checking_id:data.id,
'checking_id', pending: data.pending,
'pending', amount: data.amount,
'amount', fee: data.fee,
'fee', memo: data.memo,
'memo', time: data.time,
'time', bolt11: data.bolt11,
'bolt11', preimage: data.preimage,
'preimage', payment_hash: data.payment_hash,
'payment_hash', extra: data.extra,
'extra', wallet_id: data.wallet_id,
'wallet_id', webhook: data.webhook,
'webhook', webhook_status: data.webhook_status,
'webhook_status' }
],
data
)
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000), new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm' 'YYYY-MM-DD HH:mm'

View File

@ -1,8 +1,7 @@
import time import time
import trio import asyncio
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from quart import current_app
from typing import List, Callable from typing import List, Callable
from lnbits.settings import WALLET from lnbits.settings import WALLET
@ -25,21 +24,21 @@ def record_async(func: Callable) -> Callable:
return recorder return recorder
def run_deferred_async(): async def run_deferred_async():
for func in deferred_async: for func in deferred_async:
current_app.nursery.start_soon(catch_everything_and_restart, func) asyncio.create_task(catch_everything_and_restart(func))
async def catch_everything_and_restart(func): async def catch_everything_and_restart(func):
try: try:
await func() await func()
except trio.Cancelled: except asyncio.CancelledError:
raise # because we must pass this up raise # because we must pass this up
except Exception as exc: except Exception as exc:
print("caught exception in background task:", exc) print("caught exception in background task:", exc)
print(traceback.format_exc()) print(traceback.format_exc())
print("will restart the task in 5 seconds.") print("will restart the task in 5 seconds.")
await trio.sleep(5) await asyncio.sleep(5)
await catch_everything_and_restart(func) await catch_everything_and_restart(func)
@ -47,10 +46,10 @@ async def send_push_promise(a, b) -> None:
pass pass
invoice_listeners: List[trio.MemorySendChannel] = [] invoice_listeners: List[asyncio.Queue] = []
def register_invoice_listener(send_chan: trio.MemorySendChannel): def register_invoice_listener(send_chan: asyncio.Queue):
""" """
A method intended for extensions to call when they want to be notified about A method intended for extensions to call when they want to be notified about
new invoice payments incoming. new invoice payments incoming.
@ -65,18 +64,19 @@ async def webhook_handler():
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
internal_invoice_paid, internal_invoice_received = trio.open_memory_channel(0) internal_invoice_queue = asyncio.Queue(0)
async def internal_invoice_listener(): async def internal_invoice_listener():
async for checking_id in internal_invoice_received: while True:
current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id) checking_id = await internal_invoice_queue.get()
asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def invoice_listener(): async def invoice_listener():
async for checking_id in WALLET.paid_invoices_stream(): async for checking_id in WALLET.paid_invoices_stream():
print("> got a payment notification", checking_id) print("> got a payment notification", checking_id)
current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id) asyncio.create_task(invoice_callback_dispatcher(checking_id))
async def check_pending_payments(): async def check_pending_payments():
@ -100,7 +100,7 @@ async def check_pending_payments():
# that will be handled by the global invoice listeners, hopefully # that will be handled by the global invoice listeners, hopefully
incoming = False incoming = False
await trio.sleep(60 * 30) # every 30 minutes await asyncio.sleep(60 * 30) # every 30 minutes
async def perform_balance_checks(): async def perform_balance_checks():
@ -108,7 +108,7 @@ async def perform_balance_checks():
for bc in await get_balance_checks(): for bc in await get_balance_checks():
redeem_lnurl_withdraw(bc.wallet, bc.url) redeem_lnurl_withdraw(bc.wallet, bc.url)
await trio.sleep(60 * 60 * 6) # every 6 hours await asyncio.sleep(60 * 60 * 6) # every 6 hours
async def invoice_callback_dispatcher(checking_id: str): async def invoice_callback_dispatcher(checking_id: str):
@ -116,4 +116,4 @@ async def invoice_callback_dispatcher(checking_id: str):
if payment and payment.is_in: if payment and payment.is_in:
await payment.set_pending(False) await payment.set_pending(False)
for send_chan in invoice_listeners: for send_chan in invoice_listeners:
await send_chan.send(payment) await send_chan.put(payment)

View File

@ -0,0 +1,36 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">Error</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<h5 class="q-my-none">{{ err }}</h5>
<h4>If you believe this shouldn't be an error please bring it up on https://t.me/lnbits</h4>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>

View File

@ -1,4 +1,4 @@
import trio import asyncio
import httpx import httpx
from typing import Callable, NamedTuple from typing import Callable, NamedTuple
@ -219,12 +219,12 @@ async def btc_price(currency: str) -> float:
"to": currency.lower(), "to": currency.lower(),
} }
rates = [] rates = []
send_channel, receive_channel = trio.open_memory_channel(0) send_channel = asyncio.Queue(0)
async def controller(nursery): async def controller(nursery):
failures = 0 failures = 0
while True: while True:
rate = await receive_channel.receive() rate = await send_channel.get()
if rate: if rate:
rates.append(rate) rates.append(rate)
else: else:
@ -248,10 +248,9 @@ async def btc_price(currency: str) -> float:
except Exception: except Exception:
await send_channel.send(None) await send_channel.send(None)
async with trio.open_nursery() as nursery: # asyncio.create_task(controller, nursery)
nursery.start_soon(controller, nursery) for key, provider in exchange_rate_providers.items():
for key, provider in exchange_rate_providers.items(): asyncio.create_task(fetch_price(key, provider))
nursery.start_soon(fetch_price, key, provider)
if not rates: if not rates:
return 9999999999 return 9999999999

View File

@ -3,7 +3,7 @@ try:
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
LightningRpc = None LightningRpc = None
import trio import asyncio
import random import random
import json import json
@ -116,7 +116,7 @@ class CLightningWallet(Wallet):
raise KeyError("supplied an invalid checking_id") raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
stream = await trio.open_unix_socket(self.rpc) stream = await asyncio.open_unix_socket(self.rpc)
i = 0 i = 0
while True: while True:

View File

@ -1,4 +1,4 @@
import trio import asyncio
import json import json
import httpx import httpx
from os import getenv from os import getenv
@ -146,4 +146,4 @@ class LNbitsWallet(Wallet):
pass pass
print("lost connection to lnbits /payments/sse, retrying in 5 seconds") print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
await trio.sleep(5) await asyncio.sleep(5)

View File

@ -1,4 +1,4 @@
import trio import asyncio
import httpx import httpx
import json import json
import base64 import base64
@ -183,4 +183,4 @@ class LndRestWallet(Wallet):
pass pass
print("lost connection to lnd invoices stream, retrying in 5 seconds") print("lost connection to lnd invoices stream, retrying in 5 seconds")
await trio.sleep(5) await asyncio.sleep(5)

View File

@ -1,10 +1,9 @@
import json import json
import trio import asyncio
import httpx import httpx
from os import getenv from os import getenv
from http import HTTPStatus from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from quart import request
from .base import ( from .base import (
StatusResponse, StatusResponse,
@ -117,8 +116,9 @@ class LNPayWallet(Wallet):
return PaymentStatus(statuses[r.json()["settled"]]) return PaymentStatus(statuses[r.json()["settled"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.send, receive = trio.open_memory_channel(0) self.queue = asyncio.Queue(0)
async for value in receive: while True:
value = await self.queue.get()
yield value yield value
async def webhook_listener(self): async def webhook_listener(self):
@ -143,6 +143,6 @@ class LNPayWallet(Wallet):
) )
data = r.json() data = r.json()
if data["settled"]: if data["settled"]:
await self.send.send(lntx_id) await self.queue.put(lntx_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,4 +1,4 @@
import trio import asyncio
import json import json
import httpx import httpx
from os import getenv from os import getenv
@ -150,4 +150,4 @@ class LntxbotWallet(Wallet):
pass pass
print("lost connection to lntxbot /payments/stream, retrying in 5 seconds") print("lost connection to lntxbot /payments/stream, retrying in 5 seconds")
await trio.sleep(5) await asyncio.sleep(5)

View File

@ -1,10 +1,10 @@
import trio import asyncio
from lnbits.helpers import url_for
import hmac import hmac
import httpx import httpx
from http import HTTPStatus from http import HTTPStatus
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from quart import request, url_for
from .base import ( from .base import (
StatusResponse, StatusResponse,
@ -63,7 +63,7 @@ class OpenNodeWallet(Wallet):
json={ json={
"amount": amount, "amount": amount,
"description": memo or "", "description": memo or "",
"callback_url": url_for("webhook_listener", _external=True), "callback_url": url_for("/webhook_listener", _external=True),
}, },
timeout=40, timeout=40,
) )
@ -125,8 +125,9 @@ class OpenNodeWallet(Wallet):
return PaymentStatus(statuses[r.json()["data"]["status"]]) return PaymentStatus(statuses[r.json()["data"]["status"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.send, receive = trio.open_memory_channel(0) self.queue = asyncio.Queue(0)
async for value in receive: while True:
value = await self.queue.get()
yield value yield value
async def webhook_listener(self): async def webhook_listener(self):
@ -141,5 +142,5 @@ class OpenNodeWallet(Wallet):
print("invalid webhook, not from opennode") print("invalid webhook, not from opennode")
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
await self.send.send(charge_id) await self.queue.put(charge_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,4 +1,4 @@
import trio import asyncio
import json import json
import httpx import httpx
import random import random
@ -199,4 +199,4 @@ class SparkWallet(Wallet):
pass pass
print("lost connection to spark /stream, retrying in 5 seconds") print("lost connection to spark /stream, retrying in 5 seconds")
await trio.sleep(5) await asyncio.sleep(5)