mirror of
https://github.com/lnbits/lnbits.git
synced 2025-09-27 20:36:16 +02:00
Merge pull request #101 from lnbits/invoice-listeners
This commit is contained in:
11
.env.example
11
.env.example
@@ -35,24 +35,21 @@ LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
|
||||
# LndWallet
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=11009
|
||||
LND_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
|
||||
LND_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
|
||||
LND_READ_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/read.macaroon"
|
||||
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_GRPC_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
|
||||
LND_GRPC_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
|
||||
|
||||
# LndRestWallet
|
||||
LND_REST_ENDPOINT=https://localhost:8080/
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_REST_ADMIN_MACAROON="HEXSTRING"
|
||||
LND_REST_INVOICE_MACAROON="HEXSTRING"
|
||||
LND_REST_READ_MACAROON="HEXSTRING"
|
||||
|
||||
# LNPayWallet
|
||||
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
|
||||
LNPAY_API_KEY=LNPAY_API_KEY
|
||||
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
|
||||
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
|
||||
LNPAY_READ_KEY=LNPAY_READ_KEY
|
||||
|
||||
# LntxbotWallet
|
||||
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
|
||||
|
@@ -3,7 +3,6 @@ FROM python:3.7-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -q -r requirements.txt
|
||||
RUN pip install --no-cache-dir -q hypercorn
|
||||
COPY . /app
|
||||
|
||||
EXPOSE 5000
|
||||
|
5
Pipfile
5
Pipfile
@@ -21,10 +21,13 @@ quart-compress = "*"
|
||||
secure = "*"
|
||||
typing-extensions = "*"
|
||||
httpx = "*"
|
||||
quart-trio = "*"
|
||||
trio = "*"
|
||||
hypercorn = {extras = ["trio"], version = "*"}
|
||||
|
||||
[dev-packages]
|
||||
black = "==20.8b1"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-asyncio = "*"
|
||||
mypy = "==0.761"
|
||||
pytest-trio = "*"
|
||||
|
154
Pipfile.lock
generated
154
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "6f7d14aa2e3bc6a1319c7f0e2873151cefa741792fccc249567932a3a94263e3"
|
||||
"sha256": "76a3823f58d720ea680fdcd246f2a8b5fa16ce0a87a650e5e9fff5559dca7309"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -23,11 +23,28 @@
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"async-generator": {
|
||||
"hashes": [
|
||||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
|
||||
"sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.2.0"
|
||||
},
|
||||
"bech32": {
|
||||
"hashes": [
|
||||
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
|
||||
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"bitstring": {
|
||||
@@ -101,6 +118,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"ecdsa": {
|
||||
@@ -131,6 +149,7 @@
|
||||
"sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25",
|
||||
"sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"hpack": {
|
||||
@@ -138,6 +157,7 @@
|
||||
"sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c",
|
||||
"sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"httpcore": {
|
||||
@@ -145,21 +165,26 @@
|
||||
"sha256:72cfaa461dbdc262943ff4c9abf5b195391a03cdcc152e636adb4239b15e77e1",
|
||||
"sha256:a35dddd1f4cc34ff37788337ef507c0ad0276241ece6daf663ac9e77c0b87232"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.11.1"
|
||||
},
|
||||
"httpx": {
|
||||
"hashes": [
|
||||
"sha256:4c81dbf98a29cb4f51f415140df56542f9d4860798d713e336642e953cddd1db",
|
||||
"sha256:7b3c07bfdcdadd92020dd4c07b15932abdcf1c898422a4e98de3d19b2223310b"
|
||||
"sha256:02326f2d3c61133db31e4b88dd3432479b434e52a68d813eab6db930f13611ea",
|
||||
"sha256:254b371e3880a8e2387bf9ead6949bac797bd557fda26eba19a6153a0c06bd2b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.15.4"
|
||||
"version": "==0.15.5"
|
||||
},
|
||||
"hypercorn": {
|
||||
"extras": [
|
||||
"trio"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:6540faeba9dd44f7e74c7cc1beae3a438a7efb5f77323d1199457da46d32c2c2",
|
||||
"sha256:b5c479023757e279f954b46a4ec9dd85e58a2bcbf4d959d5601cbced593e711d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.11.0"
|
||||
},
|
||||
"hyperframe": {
|
||||
@@ -167,6 +192,7 @@
|
||||
"sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1",
|
||||
"sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"idna": {
|
||||
@@ -181,6 +207,7 @@
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -188,6 +215,7 @@
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"lnurl": {
|
||||
@@ -233,6 +261,7 @@
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"marshmallow": {
|
||||
@@ -240,8 +269,17 @@
|
||||
"sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811",
|
||||
"sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.8.0"
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:ee46c5ce42780cde85d55a61819d0e6b8cb490f1dbd749ba75ff2629771dcd2d",
|
||||
"sha256:fc7822068ba7dd0fc2532743611e8a73246708d3564e29a39f93d6ab3701b66f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"priority": {
|
||||
"hashes": [
|
||||
"sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe",
|
||||
@@ -269,6 +307,7 @@
|
||||
"sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
|
||||
"sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.6.1"
|
||||
},
|
||||
"pyscss": {
|
||||
@@ -309,6 +348,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"quart-trio": {
|
||||
"hashes": [
|
||||
"sha256:00f3b20f8d82ce7e81ead61db4efba38ed7653c7e28199defded46b663ab2595",
|
||||
"sha256:dafc8f0440d4b70fa60d24122a161d2373894d2bfa9f713d9f1df1fd508f0834"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
@@ -318,6 +365,9 @@
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
@@ -345,6 +395,7 @@
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sniffio": {
|
||||
@@ -352,8 +403,16 @@
|
||||
"sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5",
|
||||
"sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
"hashes": [
|
||||
"sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba",
|
||||
"sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
@@ -361,6 +420,14 @@
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"trio": {
|
||||
"hashes": [
|
||||
"sha256:e85cf9858e445465dfbb0e3fdf36efe92082d2df87bfe9d62585eedd6e8e9d7d",
|
||||
"sha256:fc70c74e8736d1105b3c05cc2e49b30c58755733740f9c51ae6d88a4d6d0a291"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
@@ -375,6 +442,7 @@
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"werkzeug": {
|
||||
@@ -382,6 +450,7 @@
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"wsproto": {
|
||||
@@ -389,6 +458,7 @@
|
||||
"sha256:614798c30e5dc2b3f65acc03d2d50842b97621487350ce79a80a711229edfa9d",
|
||||
"sha256:e3d190a11d9307112ba23bbe60055604949b172143969c8f641318476a9b6f1d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==0.15.0"
|
||||
}
|
||||
},
|
||||
@@ -400,11 +470,20 @@
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"async-generator": {
|
||||
"hashes": [
|
||||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
|
||||
"sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.2.0"
|
||||
},
|
||||
"black": {
|
||||
@@ -419,6 +498,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"coverage": {
|
||||
@@ -458,8 +538,16 @@
|
||||
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
||||
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==5.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
@@ -494,11 +582,20 @@
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:ee46c5ce42780cde85d55a61819d0e6b8cb490f1dbd749ba75ff2629771dcd2d",
|
||||
"sha256:fc7822068ba7dd0fc2532743611e8a73246708d3564e29a39f93d6ab3701b66f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pathspec": {
|
||||
@@ -513,6 +610,7 @@
|
||||
"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": {
|
||||
@@ -520,6 +618,7 @@
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
@@ -527,23 +626,16 @@
|
||||
"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:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33",
|
||||
"sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"
|
||||
"sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9",
|
||||
"sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.1.0"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d",
|
||||
"sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.0"
|
||||
"version": "==6.1.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -553,6 +645,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"pytest-trio": {
|
||||
"hashes": [
|
||||
"sha256:3f48cc1df66d279d705af38ad38d1639c2e2380ddffcdc3a45bb81758de61f03",
|
||||
"sha256:9bf0a490fd177a33617e8709242293fae47934de2b51f8209eb2c0545b6ca8fe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
|
||||
@@ -584,8 +684,24 @@
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5",
|
||||
"sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
"hashes": [
|
||||
"sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba",
|
||||
"sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
@@ -593,6 +709,14 @@
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"trio": {
|
||||
"hashes": [
|
||||
"sha256:e85cf9858e445465dfbb0e3fdf36efe92082d2df87bfe9d62585eedd6e8e9d7d",
|
||||
"sha256:fc70c74e8736d1105b3c05cc2e49b30c58755733740f9c51ae6d88a4d6d0a291"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||
|
2
Procfile
2
Procfile
@@ -1 +1 @@
|
||||
web: hypercorn --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
web: hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
|
@@ -34,7 +34,7 @@ You will need to copy `.env.example` to `.env`, then set variables there.
|
||||

|
||||
|
||||
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
|
||||
E.g. when you want to use LND you have to `pipenv run pip install lnd-grpc`.
|
||||
E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `pipenv run pip install pureprc`.
|
||||
|
||||
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
|
||||
|
||||
|
@@ -18,7 +18,7 @@ python3 -m venv venv
|
||||
cp .env.example .env
|
||||
./venv/bin/quart assets
|
||||
./venv/bin/quart migrate
|
||||
./venv/bin/hypercorn --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
```
|
||||
|
||||
No you can visit your LNbits at http://localhost:5000/.
|
||||
@@ -31,5 +31,6 @@ You might also need to install additional packages, depending on the chosen back
|
||||
E.g. when you want to use LND you have to run:
|
||||
|
||||
```sh
|
||||
./venv/bin/pip install lnd-grpc
|
||||
./venv/bin/pip install lndgrpc
|
||||
./venv/bin/pip install purerpc
|
||||
```
|
||||
|
@@ -29,7 +29,7 @@ Using this wallet requires the installation of the `pylightning` Python package.
|
||||
|
||||
### LND (gRPC)
|
||||
|
||||
Using this wallet requires the installation of the `lnd-grpc` Python package.
|
||||
Using this wallet requires the installation of the `lndgrpc` and `purerpc` Python packages.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
|
@@ -1,24 +1,26 @@
|
||||
import trio # type: ignore
|
||||
import importlib
|
||||
|
||||
from quart import Quart, g
|
||||
from quart import g
|
||||
from quart_trio import QuartTrio
|
||||
from quart_cors import cors # type: ignore
|
||||
from quart_compress import Compress # type: ignore
|
||||
from secure import SecureHeaders # type: ignore
|
||||
|
||||
from .commands import db_migrate, handle_assets
|
||||
from .core import core_app
|
||||
from .db import open_db
|
||||
from .db import open_db, open_ext_db
|
||||
from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
|
||||
from .proxy_fix import ASGIProxyFix
|
||||
|
||||
secure_headers = SecureHeaders(hsts=False)
|
||||
|
||||
|
||||
def create_app(config_object="lnbits.settings") -> Quart:
|
||||
def create_app(config_object="lnbits.settings") -> QuartTrio:
|
||||
"""Create application factory.
|
||||
:param config_object: The configuration object to use.
|
||||
"""
|
||||
app = Quart(__name__, static_folder="static")
|
||||
app = QuartTrio(__name__, static_folder="static")
|
||||
app.config.from_object(config_object)
|
||||
app.asgi_http_class = ASGIProxyFix
|
||||
|
||||
@@ -30,29 +32,40 @@ def create_app(config_object="lnbits.settings") -> Quart:
|
||||
register_filters(app)
|
||||
register_commands(app)
|
||||
register_request_hooks(app)
|
||||
register_async_tasks(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_blueprints(app: Quart) -> None:
|
||||
def register_blueprints(app: QuartTrio) -> None:
|
||||
"""Register Flask blueprints / LNbits extensions."""
|
||||
app.register_blueprint(core_app)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
try:
|
||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||
app.register_blueprint(getattr(ext_module, f"{ext.code}_ext"), url_prefix=f"/{ext.code}")
|
||||
bp = getattr(ext_module, f"{ext.code}_ext")
|
||||
|
||||
@bp.before_request
|
||||
async def before_request():
|
||||
g.ext_db = open_ext_db(ext.code)
|
||||
|
||||
@bp.teardown_request
|
||||
async def after_request(exc):
|
||||
g.ext_db.__exit__(type(exc), exc, None)
|
||||
|
||||
app.register_blueprint(bp, url_prefix=f"/{ext.code}")
|
||||
except Exception:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
|
||||
|
||||
|
||||
def register_commands(app: Quart):
|
||||
def register_commands(app: QuartTrio):
|
||||
"""Register Click commands."""
|
||||
app.cli.add_command(db_migrate)
|
||||
app.cli.add_command(handle_assets)
|
||||
|
||||
|
||||
def register_assets(app: Quart):
|
||||
def register_assets(app: QuartTrio):
|
||||
"""Serve each vendored asset separately or a bundle."""
|
||||
|
||||
@app.before_request
|
||||
@@ -65,13 +78,13 @@ def register_assets(app: Quart):
|
||||
g.VENDORED_CSS = ["/static/bundle.css"]
|
||||
|
||||
|
||||
def register_filters(app: Quart):
|
||||
def register_filters(app: QuartTrio):
|
||||
"""Jinja filters."""
|
||||
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
|
||||
|
||||
|
||||
def register_request_hooks(app: Quart):
|
||||
def register_request_hooks(app: QuartTrio):
|
||||
"""Open the core db for each request so everything happens in a big transaction"""
|
||||
|
||||
@app.before_request
|
||||
@@ -86,3 +99,20 @@ def register_request_hooks(app: Quart):
|
||||
@app.teardown_request
|
||||
async def after_request(exc):
|
||||
g.db.__exit__(type(exc), exc, None)
|
||||
|
||||
|
||||
def register_async_tasks(app):
|
||||
from lnbits.core.tasks import invoice_listener, webhook_handler
|
||||
|
||||
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
async def webhook_listener():
|
||||
return await webhook_handler()
|
||||
|
||||
@app.before_serving
|
||||
async def listeners():
|
||||
app.nursery.start_soon(invoice_listener)
|
||||
print("started invoice_listener")
|
||||
|
||||
@app.after_serving
|
||||
async def stop_listeners():
|
||||
pass
|
||||
|
@@ -131,6 +131,19 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
|
||||
# ---------------
|
||||
|
||||
|
||||
def get_standalone_payment(checking_id: str) -> Optional[Payment]:
|
||||
row = g.db.fetchone(
|
||||
"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE checking_id = ?
|
||||
""",
|
||||
(checking_id,),
|
||||
)
|
||||
|
||||
return Payment.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]:
|
||||
row = g.db.fetchone(
|
||||
"""
|
||||
|
@@ -2,6 +2,8 @@ import json
|
||||
from typing import List, NamedTuple, Optional, Dict
|
||||
from sqlite3 import Row
|
||||
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
id: str
|
||||
@@ -113,6 +115,17 @@ class Payment(NamedTuple):
|
||||
|
||||
update_payment_status(self.checking_id, pending)
|
||||
|
||||
def check_pending(self) -> None:
|
||||
if self.is_uncheckable:
|
||||
return
|
||||
|
||||
if self.is_out:
|
||||
pending = WALLET.get_payment_status(self.checking_id)
|
||||
else:
|
||||
pending = WALLET.get_invoice_status(self.checking_id)
|
||||
|
||||
self.set_pending(pending.pending)
|
||||
|
||||
def delete(self) -> None:
|
||||
from .crud import delete_payment
|
||||
|
||||
|
@@ -1,11 +1,17 @@
|
||||
import asyncio
|
||||
from typing import Optional, Awaitable
|
||||
from quart import Quart, Request, g
|
||||
import trio # type: ignore
|
||||
from http import HTTPStatus
|
||||
from typing import Optional, Tuple, List, Callable, Awaitable
|
||||
from quart import Request, g
|
||||
from quart_trio import QuartTrio
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
from lnbits.db import open_db
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
main_app: Optional[Quart] = None
|
||||
from .models import Payment
|
||||
from .crud import get_standalone_payment
|
||||
|
||||
main_app: Optional[QuartTrio] = None
|
||||
|
||||
|
||||
def grab_app_for_later(state):
|
||||
@@ -13,21 +19,60 @@ def grab_app_for_later(state):
|
||||
main_app = state.app
|
||||
|
||||
|
||||
def run_on_pseudo_request(awaitable: Awaitable):
|
||||
async def run(awaitable):
|
||||
fk = Request(
|
||||
"GET",
|
||||
"http",
|
||||
"/background/pseudo",
|
||||
b"",
|
||||
Headers([("host", "lnbits.background")]),
|
||||
"",
|
||||
"1.1",
|
||||
send_push_promise=lambda x, h: None,
|
||||
)
|
||||
async with main_app.request_context(fk):
|
||||
g.db = open_db()
|
||||
await awaitable
|
||||
async def send_push_promise(a, b) -> None:
|
||||
pass
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(run(awaitable))
|
||||
|
||||
async def run_on_pseudo_request(func: Callable, *args):
|
||||
fk = Request(
|
||||
"GET",
|
||||
"http",
|
||||
"/background/pseudo",
|
||||
b"",
|
||||
Headers([("host", "lnbits.background")]),
|
||||
"",
|
||||
"1.1",
|
||||
send_push_promise=send_push_promise,
|
||||
)
|
||||
assert main_app
|
||||
|
||||
async def run():
|
||||
async with main_app.request_context(fk):
|
||||
with open_db() as g.db: # type: ignore
|
||||
await func(*args)
|
||||
|
||||
async with trio.open_nursery() as nursery:
|
||||
nursery.start_soon(run)
|
||||
|
||||
|
||||
invoice_listeners: List[Tuple[str, Callable[[Payment], Awaitable[None]]]] = []
|
||||
|
||||
|
||||
def register_invoice_listener(ext_name: str, cb: Callable[[Payment], Awaitable[None]]):
|
||||
"""
|
||||
A method intended for extensions to call when they want to be notified about
|
||||
new invoice payments incoming.
|
||||
"""
|
||||
print(f"registering {ext_name} invoice_listener callback: {cb}")
|
||||
invoice_listeners.append((ext_name, cb))
|
||||
|
||||
|
||||
async def webhook_handler():
|
||||
handler = getattr(WALLET, "webhook_listener", None)
|
||||
if handler:
|
||||
return await handler()
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
async def invoice_listener():
|
||||
async for checking_id in WALLET.paid_invoices_stream():
|
||||
await run_on_pseudo_request(invoice_callback_dispatcher, checking_id)
|
||||
|
||||
|
||||
async def invoice_callback_dispatcher(checking_id: str):
|
||||
payment = get_standalone_payment(checking_id)
|
||||
if payment and payment.is_in:
|
||||
payment.set_pending(False)
|
||||
for ext_name, cb in invoice_listeners:
|
||||
with open_ext_db(ext_name) as g.ext_db: # type: ignore
|
||||
await cb(payment)
|
||||
|
@@ -7,7 +7,6 @@ from lnbits.core import core_app
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.core.crud import delete_expired_invoices
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
|
||||
@core_app.route("/api/v1/wallet", methods=["GET"])
|
||||
@@ -32,10 +31,7 @@ async def api_payments():
|
||||
delete_expired_invoices()
|
||||
|
||||
for payment in g.wallet.get_payments(complete=False, pending=True, exclude_uncheckable=True):
|
||||
if payment.is_out:
|
||||
payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
|
||||
else:
|
||||
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
|
||||
payment.check_pending()
|
||||
|
||||
return jsonify(g.wallet.get_payments(pending=True)), HTTPStatus.OK
|
||||
|
||||
@@ -123,17 +119,8 @@ async def api_payment(payment_hash):
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
try:
|
||||
if payment.is_uncheckable:
|
||||
pass
|
||||
elif payment.is_out:
|
||||
is_paid = not WALLET.get_payment_status(payment.checking_id).pending
|
||||
elif payment.is_in:
|
||||
is_paid = not WALLET.get_invoice_status(payment.checking_id).pending
|
||||
payment.check_pending()
|
||||
except Exception:
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
if is_paid:
|
||||
payment.set_pending(False)
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
return jsonify({"paid": not payment.pending}), HTTPStatus.OK
|
||||
|
@@ -123,6 +123,6 @@ async def lnurlwallet():
|
||||
user = get_user(account.id)
|
||||
wallet = create_wallet(user_id=user.id)
|
||||
|
||||
run_on_pseudo_request(redeem_lnurl_withdraw(wallet.id, withdraw_res, "LNbits initial funding: voucher redeem."))
|
||||
run_on_pseudo_request(redeem_lnurl_withdraw, wallet.id, withdraw_res, "LNbits initial funding: voucher redeem.")
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
|
@@ -10,20 +10,26 @@ class Database:
|
||||
self.connection = sqlite3.connect(db_path)
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
self.cursor = self.connection.cursor()
|
||||
self.closed = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.closed:
|
||||
return
|
||||
|
||||
if exc_val:
|
||||
self.connection.rollback()
|
||||
self.cursor.close()
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
else:
|
||||
self.connection.commit()
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
|
||||
self.closed = True
|
||||
|
||||
def commit(self):
|
||||
self.connection.commit()
|
||||
|
||||
|
@@ -6,3 +6,9 @@ lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", te
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .lnurl import * # noqa
|
||||
from .tasks import on_invoice_paid
|
||||
|
||||
from lnbits.core.tasks import register_invoice_listener
|
||||
|
||||
register_invoice_listener("lnurlp", on_invoice_paid)
|
||||
|
@@ -1,11 +1,12 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import open_ext_db
|
||||
|
||||
from .models import PayLink
|
||||
|
||||
|
||||
def create_pay_link(*, wallet_id: str, description: str, amount: int) -> PayLink:
|
||||
def create_pay_link(*, wallet_id: str, description: str, amount: int, webhook_url: str) -> Optional[PayLink]:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute(
|
||||
"""
|
||||
@@ -14,26 +15,36 @@ def create_pay_link(*, wallet_id: str, description: str, amount: int) -> PayLink
|
||||
description,
|
||||
amount,
|
||||
served_meta,
|
||||
served_pr
|
||||
served_pr,
|
||||
webhook_url
|
||||
)
|
||||
VALUES (?, ?, ?, 0, 0)
|
||||
VALUES (?, ?, ?, 0, 0, ?)
|
||||
""",
|
||||
(wallet_id, description, amount),
|
||||
(wallet_id, description, amount, webhook_url),
|
||||
)
|
||||
link_id = db.cursor.lastrowid
|
||||
return get_pay_link(link_id)
|
||||
|
||||
|
||||
def get_pay_link(link_id: str) -> Optional[PayLink]:
|
||||
def get_pay_link(link_id: int) -> Optional[PayLink]:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def get_pay_link_by_hash(unique_hash: str) -> Optional[PayLink]:
|
||||
def get_pay_link_by_invoice(payment_hash: str) -> Optional[PayLink]:
|
||||
# this excludes invoices with webhooks that have been sent already
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
row = db.fetchone("SELECT * FROM pay_links WHERE unique_hash = ?", (unique_hash,))
|
||||
row = db.fetchone(
|
||||
"""
|
||||
SELECT pay_links.* FROM pay_links
|
||||
INNER JOIN invoices ON invoices.pay_link = pay_links.id
|
||||
WHERE payment_hash = ? AND webhook_sent IS NULL
|
||||
""",
|
||||
(payment_hash,),
|
||||
)
|
||||
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
@@ -49,7 +60,7 @@ def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
||||
return [PayLink.from_row(row) for row in rows]
|
||||
|
||||
|
||||
def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
@@ -59,7 +70,7 @@ def update_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
@@ -69,6 +80,30 @@ def increment_pay_link(link_id: str, **kwargs) -> Optional[PayLink]:
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
def delete_pay_link(link_id: str) -> None:
|
||||
def delete_pay_link(link_id: int) -> None:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
|
||||
|
||||
|
||||
def save_link_invoice(link_id: int, payment_request: str) -> None:
|
||||
inv = bolt11.decode(payment_request)
|
||||
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO invoices (pay_link, payment_hash, expiry)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(link_id, inv.payment_hash, inv.expiry),
|
||||
)
|
||||
|
||||
|
||||
def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
||||
with open_ext_db("lnurlp") as db:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE invoices SET webhook_sent = ?
|
||||
WHERE payment_hash = ?
|
||||
""",
|
||||
(status, payment_hash),
|
||||
)
|
||||
|
48
lnbits/extensions/lnurlp/lnurl.py
Normal file
48
lnbits/extensions/lnurlp/lnurl.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from quart import jsonify, url_for
|
||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
|
||||
from . import lnurlp_ext
|
||||
from .crud import increment_pay_link, save_link_invoice
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
|
||||
async def api_lnurl_response(link_id):
|
||||
link = increment_pay_link(link_id, served_meta=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
url = url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True)
|
||||
|
||||
resp = LnurlPayResponse(
|
||||
callback=url,
|
||||
min_sendable=link.amount * 1000,
|
||||
max_sendable=link.amount * 1000,
|
||||
metadata=link.lnurlpay_metadata,
|
||||
)
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
|
||||
async def api_lnurl_callback(link_id):
|
||||
link = increment_pay_link(link_id, served_pr=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
_, payment_request = create_invoice(
|
||||
wallet_id=link.wallet,
|
||||
amount=link.amount,
|
||||
memo=link.description,
|
||||
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(),
|
||||
extra={"tag": "lnurlp"},
|
||||
)
|
||||
|
||||
save_link_invoice(link_id, payment_request)
|
||||
|
||||
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
@@ -14,3 +14,22 @@ def m001_initial(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def m002_webhooks_and_success_actions(db):
|
||||
"""
|
||||
Webhooks and success actions.
|
||||
"""
|
||||
db.execute("ALTER TABLE pay_links ADD COLUMN webhook_url TEXT;")
|
||||
db.execute("ALTER TABLE pay_links ADD COLUMN success_text TEXT;")
|
||||
db.execute("ALTER TABLE pay_links ADD COLUMN success_url TEXT;")
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE invoices (
|
||||
pay_link INTEGER NOT NULL REFERENCES pay_links (id),
|
||||
payment_hash TEXT NOT NULL,
|
||||
webhook_sent INT, -- null means not sent, otherwise store status
|
||||
expiry INT
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
@@ -7,12 +7,15 @@ from typing import NamedTuple
|
||||
|
||||
|
||||
class PayLink(NamedTuple):
|
||||
id: str
|
||||
id: int
|
||||
wallet: str
|
||||
description: str
|
||||
amount: int
|
||||
served_meta: int
|
||||
served_pr: int
|
||||
webhook_url: str
|
||||
success_text: str
|
||||
success_url: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "PayLink":
|
||||
@@ -27,3 +30,9 @@ class PayLink(NamedTuple):
|
||||
@property
|
||||
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
|
||||
|
||||
|
||||
class Invoice(NamedTuple):
|
||||
payment_hash: str
|
||||
link_id: int
|
||||
webhook_sent: bool
|
||||
|
31
lnbits/extensions/lnurlp/tasks.py
Normal file
31
lnbits/extensions/lnurlp/tasks.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import httpx
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
|
||||
from .crud import get_pay_link_by_invoice, mark_webhook_sent
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
print(payment)
|
||||
islnurlp = "lnurlp" == payment.extra.get("tag")
|
||||
if islnurlp:
|
||||
pay_link = get_pay_link_by_invoice(payment.payment_hash)
|
||||
if not pay_link:
|
||||
# no pay_link or this webhook has already been sent
|
||||
return
|
||||
if pay_link.webhook_url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
pay_link.webhook_url,
|
||||
json={
|
||||
"payment_hash": payment.payment_hash,
|
||||
"payment_request": payment.bolt11,
|
||||
"amount": payment.amount,
|
||||
"lnurlp": pay_link.id,
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.RequestError:
|
||||
mark_webhook_sent(payment.payment_hash, -1)
|
@@ -131,6 +131,13 @@
|
||||
type="number"
|
||||
label="Amount (sat) *"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="formDialog.data.webhook_url"
|
||||
type="text"
|
||||
label="Webhook URL (optional)"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
@@ -149,7 +156,8 @@
|
||||
(
|
||||
formDialog.data.amount == null ||
|
||||
formDialog.data.amount < 1
|
||||
)"
|
||||
)
|
||||
"
|
||||
type="submit"
|
||||
>Create pay link</q-btn
|
||||
>
|
||||
@@ -174,6 +182,7 @@
|
||||
<p style="word-break: break-all">
|
||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }} sat<br />
|
||||
<strong>Webhook:</strong> {{ qrCodeDialog.data.webhook_url }}<br />
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
@@ -248,6 +257,12 @@
|
||||
align: 'right',
|
||||
label: 'Amount (sat)',
|
||||
field: 'amount'
|
||||
},
|
||||
{
|
||||
name: 'webhook_url',
|
||||
align: 'left',
|
||||
label: 'Webhook URL',
|
||||
field: 'webhook_url'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
@@ -331,7 +346,7 @@
|
||||
'PUT',
|
||||
'/lnurlp/api/v1/links/' + data.id,
|
||||
wallet.adminkey,
|
||||
_.pick(data, 'description', 'amount')
|
||||
_.pick(data, 'description', 'amount', 'webhook_url')
|
||||
)
|
||||
.then(function (response) {
|
||||
self.payLinks = _.reject(self.payLinks, function (obj) {
|
||||
|
@@ -1,11 +1,8 @@
|
||||
import hashlib
|
||||
from quart import g, jsonify, request, url_for
|
||||
from quart import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from lnbits.extensions.lnurlp import lnurlp_ext
|
||||
@@ -14,7 +11,6 @@ from .crud import (
|
||||
get_pay_link,
|
||||
get_pay_links,
|
||||
update_pay_link,
|
||||
increment_pay_link,
|
||||
delete_pay_link,
|
||||
)
|
||||
|
||||
@@ -60,6 +56,7 @@ async def api_link_retrieve(link_id):
|
||||
schema={
|
||||
"description": {"type": "string", "empty": False, "required": True},
|
||||
"amount": {"type": "integer", "min": 1, "required": True},
|
||||
"webhook_url": {"type": "string", "required": False},
|
||||
}
|
||||
)
|
||||
async def api_link_create_or_update(link_id=None):
|
||||
@@ -93,39 +90,3 @@ async def api_link_delete(link_id):
|
||||
delete_pay_link(link_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
|
||||
async def api_lnurl_response(link_id):
|
||||
link = increment_pay_link(link_id, served_meta=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
url = url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True)
|
||||
|
||||
resp = LnurlPayResponse(
|
||||
callback=url,
|
||||
min_sendable=link.amount * 1000,
|
||||
max_sendable=link.amount * 1000,
|
||||
metadata=link.lnurlpay_metadata,
|
||||
)
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
|
||||
async def api_lnurl_callback(link_id):
|
||||
link = increment_pay_link(link_id, served_pr=1)
|
||||
if not link:
|
||||
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
|
||||
|
||||
_, payment_request = create_invoice(
|
||||
wallet_id=link.wallet,
|
||||
amount=link.amount,
|
||||
memo=link.description,
|
||||
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(),
|
||||
extra={"tag": "lnurlp"},
|
||||
)
|
||||
resp = LnurlPayActionResponse(pr=payment_request, success_action=None, routes=[])
|
||||
|
||||
return jsonify(resp.dict()), HTTPStatus.OK
|
||||
|
@@ -5,10 +5,10 @@ from urllib.parse import urlparse
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
from quart import Request
|
||||
from quart.asgi import ASGIHTTPConnection
|
||||
from quart_trio.asgi import TrioASGIHTTPConnection
|
||||
|
||||
|
||||
class ASGIProxyFix(ASGIHTTPConnection):
|
||||
class ASGIProxyFix(TrioASGIHTTPConnection):
|
||||
def _create_request_from_scope(self, send: Callable) -> Request:
|
||||
headers = Headers()
|
||||
headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import NamedTuple, Optional, AsyncGenerator
|
||||
|
||||
|
||||
class InvoiceResponse(NamedTuple):
|
||||
@@ -43,6 +43,15 @@ class Wallet(ABC):
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
this is an async function, but here it is noted without the 'async'
|
||||
prefix because mypy has a bug identifying the signature of abstract
|
||||
methods.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Unsupported(Exception):
|
||||
pass
|
||||
|
@@ -1,12 +1,14 @@
|
||||
try:
|
||||
from lightning import LightningRpc # type: ignore
|
||||
from lightning import LightningRpc, RpcError # type: ignore
|
||||
except ImportError: # pragma: nocover
|
||||
LightningRpc = None
|
||||
|
||||
import trio # type: ignore
|
||||
import random
|
||||
import json
|
||||
|
||||
from os import getenv
|
||||
from typing import Optional
|
||||
from typing import Optional, AsyncGenerator
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
||||
|
||||
|
||||
@@ -15,34 +17,63 @@ class CLightningWallet(Wallet):
|
||||
if LightningRpc is None: # pragma: nocover
|
||||
raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.")
|
||||
|
||||
self.l1 = LightningRpc(getenv("CLIGHTNING_RPC"))
|
||||
self.rpc = getenv("CLIGHTNING_RPC")
|
||||
self.ln = LightningRpc(self.rpc)
|
||||
|
||||
# check description_hash support (could be provided by a plugin)
|
||||
self.supports_description_hash = False
|
||||
try:
|
||||
answer = self.ln.help("invoicewithdescriptionhash")
|
||||
if answer["help"][0]["command"].startswith(
|
||||
"invoicewithdescriptionhash msatoshi label description_hash",
|
||||
):
|
||||
self.supports_description_hash = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# check last payindex so we can listen from that point on
|
||||
self.last_pay_index = 0
|
||||
invoices = self.ln.listinvoices()
|
||||
for inv in invoices["invoices"][::-1]:
|
||||
if "pay_index" in inv:
|
||||
self.last_pay_index = inv["pay_index"]
|
||||
break
|
||||
|
||||
def create_invoice(
|
||||
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
|
||||
) -> InvoiceResponse:
|
||||
if description_hash:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
label = "lbl{}".format(random.random())
|
||||
r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True)
|
||||
ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
msat = amount * 1000
|
||||
|
||||
try:
|
||||
if description_hash:
|
||||
if not self.supports_description_hash:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
params = [msat, label, description_hash.hex()]
|
||||
r = self.ln.call("invoicewithdescriptionhash", params)
|
||||
return InvoiceResponse(True, label, r["bolt11"], "")
|
||||
else:
|
||||
r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True)
|
||||
return InvoiceResponse(True, label, r["bolt11"], "")
|
||||
except RpcError as exc:
|
||||
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
||||
return InvoiceResponse(False, label, None, error_message)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = self.l1.pay(bolt11)
|
||||
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
r = self.ln.pay(bolt11)
|
||||
return PaymentResponse(True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.l1.listinvoices(checking_id)
|
||||
r = self.ln.listinvoices(checking_id)
|
||||
if not r["invoices"]:
|
||||
return PaymentStatus(False)
|
||||
if r["invoices"][0]["label"] == checking_id:
|
||||
return PaymentStatus(r["pays"][0]["status"] == "paid")
|
||||
return PaymentStatus(r["invoices"][0]["status"] == "paid")
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = self.l1.listpays(payment_hash=checking_id)
|
||||
r = self.ln.listpays(payment_hash=checking_id)
|
||||
if not r["pays"]:
|
||||
return PaymentStatus(False)
|
||||
if r["pays"][0]["payment_hash"] == checking_id:
|
||||
@@ -53,3 +84,27 @@ class CLightningWallet(Wallet):
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
stream = await trio.open_unix_socket(self.rpc)
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
call = json.dumps(
|
||||
{
|
||||
"method": "waitanyinvoice",
|
||||
"id": 0,
|
||||
"params": [self.last_pay_index],
|
||||
}
|
||||
)
|
||||
|
||||
await stream.send_all(call.encode("utf-8"))
|
||||
|
||||
data = await stream.receive_some()
|
||||
paid = json.loads(data.decode("ascii"))
|
||||
|
||||
paid = self.ln.waitanyinvoice(self.last_pay_index)
|
||||
self.last_pay_index = paid["pay_index"]
|
||||
yield paid["label"]
|
||||
|
||||
i += 1
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import trio # type: ignore
|
||||
from os import getenv
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
from requests import get, post
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
@@ -64,3 +65,8 @@ class LNbitsWallet(Wallet):
|
||||
return PaymentStatus(None)
|
||||
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
print("lnbits does not support paid invoices stream yet")
|
||||
await trio.sleep(5)
|
||||
yield ""
|
||||
|
@@ -1,92 +1,181 @@
|
||||
try:
|
||||
import lnd_grpc # type: ignore
|
||||
import lndgrpc # type: ignore
|
||||
from lndgrpc.common import ln # type: ignore
|
||||
except ImportError: # pragma: nocover
|
||||
lnd_grpc = None
|
||||
lndgrpc = None
|
||||
|
||||
try:
|
||||
import purerpc # type: ignore
|
||||
except ImportError: # pragma: nocover
|
||||
purerpc = None
|
||||
|
||||
import binascii
|
||||
import base64
|
||||
import hashlib
|
||||
from os import getenv
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
||||
|
||||
def parse_checking_id(checking_id: str) -> bytes:
|
||||
return base64.b64decode(
|
||||
checking_id.replace("_", "/"),
|
||||
)
|
||||
|
||||
|
||||
def stringify_checking_id(r_hash: bytes) -> str:
|
||||
return (
|
||||
base64.b64encode(
|
||||
r_hash,
|
||||
)
|
||||
.decode("utf-8")
|
||||
.replace("/", "_")
|
||||
)
|
||||
|
||||
|
||||
class LndWallet(Wallet):
|
||||
def __init__(self):
|
||||
if lnd_grpc is None: # pragma: nocover
|
||||
raise ImportError("The `lnd-grpc` library must be installed to use `LndWallet`.")
|
||||
if lndgrpc is None: # pragma: nocover
|
||||
raise ImportError("The `lndgrpc` library must be installed to use `LndWallet`.")
|
||||
|
||||
if purerpc is None: # pragma: nocover
|
||||
raise ImportError("The `purerpc` library must be installed to use `LndWallet`.")
|
||||
|
||||
endpoint = getenv("LND_GRPC_ENDPOINT")
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.port = getenv("LND_GRPC_PORT")
|
||||
self.auth_admin = getenv("LND_ADMIN_MACAROON")
|
||||
self.auth_invoice = getenv("LND_INVOICE_MACAROON")
|
||||
self.auth_read = getenv("LND_READ_MACAROON")
|
||||
self.auth_cert = getenv("LND_CERT")
|
||||
self.port = int(getenv("LND_GRPC_PORT"))
|
||||
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
|
||||
self.auth_admin = getenv("LND_GRPC_ADMIN_MACAROON") or getenv("LND_ADMIN_MACAROON")
|
||||
self.auth_invoices = getenv("LND_GRPC_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
|
||||
network = getenv("LND_GRPC_NETWORK", "mainnet")
|
||||
|
||||
self.admin_rpc = lndgrpc.LNDClient(
|
||||
f"{self.endpoint}:{self.port}",
|
||||
cert_filepath=self.cert_path,
|
||||
network=network,
|
||||
macaroon_filepath=self.auth_admin,
|
||||
)
|
||||
|
||||
self.invoices_rpc = lndgrpc.LNDClient(
|
||||
f"{self.endpoint}:{self.port}",
|
||||
cert_filepath=self.cert_path,
|
||||
network=network,
|
||||
macaroon_filepath=self.auth_invoices,
|
||||
)
|
||||
|
||||
def create_invoice(
|
||||
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
|
||||
) -> InvoiceResponse:
|
||||
lnd_rpc = lnd_grpc.Client(
|
||||
lnd_dir=None,
|
||||
macaroon_path=self.auth_invoice,
|
||||
tls_cert_path=self.auth_cert,
|
||||
network="mainnet",
|
||||
grpc_host=self.endpoint,
|
||||
grpc_port=self.port,
|
||||
)
|
||||
|
||||
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
||||
|
||||
if description_hash:
|
||||
params["description_hash"] = description_hash # as bytes directly
|
||||
else:
|
||||
params["memo"] = memo or ""
|
||||
lndResponse = lnd_rpc.add_invoice(**params)
|
||||
decoded_hash = base64.b64encode(lndResponse.r_hash).decode("utf-8").replace("/", "_")
|
||||
ok, checking_id, payment_request, error_message = True, decoded_hash, str(lndResponse.payment_request), None
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
try:
|
||||
req = ln.Invoice(**params)
|
||||
resp = self.invoices_rpc._ln_stub.AddInvoice(req)
|
||||
except Exception as exc:
|
||||
error_message = str(exc)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
checking_id = stringify_checking_id(resp.r_hash)
|
||||
payment_request = str(resp.payment_request)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
lnd_rpc = lnd_grpc.Client(
|
||||
lnd_dir=None,
|
||||
macaroon_path=self.auth_admin,
|
||||
tls_cert_path=self.auth_cert,
|
||||
network="mainnet",
|
||||
grpc_host=self.endpoint,
|
||||
grpc_port=self.port,
|
||||
)
|
||||
resp = self.admin_rpc.send_payment(payment_request=bolt11)
|
||||
|
||||
payinvoice = lnd_rpc.pay_invoice(
|
||||
payment_request=bolt11,
|
||||
)
|
||||
if resp.payment_error:
|
||||
return PaymentResponse(False, "", 0, resp.payment_error)
|
||||
|
||||
ok, checking_id, fee_msat, error_message = True, None, 0, None
|
||||
|
||||
if payinvoice.payment_error:
|
||||
ok, error_message = False, payinvoice.payment_error
|
||||
else:
|
||||
checking_id = base64.b64encode(payinvoice.payment_hash).decode("utf-8").replace("/", "_")
|
||||
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
r_hash = hashlib.sha256(resp.payment_preimage).digest()
|
||||
checking_id = stringify_checking_id(r_hash)
|
||||
return PaymentResponse(True, checking_id, 0, None)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
r_hash = parse_checking_id(checking_id)
|
||||
if len(r_hash) != 32:
|
||||
raise binascii.Error
|
||||
except binascii.Error:
|
||||
# this may happen if we switch between backend wallets
|
||||
# that use different checking_id formats
|
||||
return PaymentStatus(None)
|
||||
|
||||
check_id = base64.b64decode(checking_id.replace("_", "/"))
|
||||
print(check_id)
|
||||
lnd_rpc = lnd_grpc.Client(
|
||||
lnd_dir=None,
|
||||
macaroon_path=self.auth_invoice,
|
||||
tls_cert_path=self.auth_cert,
|
||||
network="mainnet",
|
||||
grpc_host=self.endpoint,
|
||||
grpc_port=self.port,
|
||||
)
|
||||
|
||||
for _response in lnd_rpc.subscribe_single_invoice(check_id):
|
||||
if _response.state == 1:
|
||||
return PaymentStatus(True)
|
||||
resp = self.invoices_rpc.lookup_invoice(r_hash.hex())
|
||||
if resp.settled:
|
||||
return PaymentStatus(True)
|
||||
|
||||
return PaymentStatus(None)
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
return PaymentStatus(True)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
async with purerpc.secure_channel(
|
||||
self.endpoint,
|
||||
self.port,
|
||||
get_ssl_context(self.cert_path),
|
||||
) as channel:
|
||||
client = purerpc.Client("lnrpc.Lightning", channel)
|
||||
subscribe_invoices = client.get_method_stub(
|
||||
"SubscribeInvoices",
|
||||
purerpc.RPCSignature(
|
||||
purerpc.Cardinality.UNARY_STREAM,
|
||||
ln.InvoiceSubscription,
|
||||
ln.Invoice,
|
||||
),
|
||||
)
|
||||
macaroon = load_macaroon(self.auth_admin)
|
||||
|
||||
async for inv in subscribe_invoices(
|
||||
ln.InvoiceSubscription(),
|
||||
metadata=[("macaroon", macaroon)],
|
||||
):
|
||||
if not inv.settled:
|
||||
continue
|
||||
|
||||
checking_id = stringify_checking_id(inv.r_hash)
|
||||
yield checking_id
|
||||
|
||||
|
||||
def get_ssl_context(cert_path: str):
|
||||
import ssl
|
||||
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
context.options |= ssl.OP_NO_SSLv2
|
||||
context.options |= ssl.OP_NO_SSLv3
|
||||
context.options |= ssl.OP_NO_TLSv1
|
||||
context.options |= ssl.OP_NO_TLSv1_1
|
||||
context.options |= ssl.OP_NO_COMPRESSION
|
||||
context.set_ciphers(
|
||||
":".join(
|
||||
[
|
||||
"ECDHE+AESGCM",
|
||||
"ECDHE+CHACHA20",
|
||||
"DHE+AESGCM",
|
||||
"DHE+CHACHA20",
|
||||
"ECDH+AESGCM",
|
||||
"DH+AESGCM",
|
||||
"ECDH+AES",
|
||||
"DH+AES",
|
||||
"RSA+AESGCM",
|
||||
"RSA+AES",
|
||||
"!aNULL",
|
||||
"!eNULL",
|
||||
"!MD5",
|
||||
"!DSS",
|
||||
]
|
||||
)
|
||||
)
|
||||
context.load_verify_locations(capath=cert_path)
|
||||
return context
|
||||
|
||||
|
||||
def load_macaroon(macaroon_path: str):
|
||||
with open(macaroon_path, "rb") as f:
|
||||
macaroon_bytes = f.read()
|
||||
return macaroon_bytes.hex()
|
||||
|
@@ -1,7 +1,9 @@
|
||||
from os import getenv
|
||||
from typing import Optional, Dict
|
||||
import httpx
|
||||
import json
|
||||
import base64
|
||||
from requests import get, post
|
||||
from os import getenv
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
||||
|
||||
@@ -11,11 +13,16 @@ class LndRestWallet(Wallet):
|
||||
def __init__(self):
|
||||
|
||||
endpoint = getenv("LND_REST_ENDPOINT")
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
print(self.endpoint)
|
||||
self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_REST_ADMIN_MACAROON")}
|
||||
self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_REST_INVOICE_MACAROON")}
|
||||
self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_REST_READ_MACAROON")}
|
||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
endpoint = "https://" + endpoint if not endpoint.startswith("http") else endpoint
|
||||
self.endpoint = endpoint
|
||||
|
||||
self.auth_admin = {
|
||||
"Grpc-Metadata-macaroon": getenv("LND_ADMIN_MACAROON") or getenv("LND_REST_ADMIN_MACAROON"),
|
||||
}
|
||||
self.auth_invoice = {
|
||||
"Grpc-Metadata-macaroon": getenv("LND_INVOICE_MACAROON") or getenv("LND_REST_INVOICE_MACAROON")
|
||||
}
|
||||
self.auth_cert = getenv("LND_REST_CERT")
|
||||
|
||||
def create_invoice(
|
||||
@@ -30,84 +37,93 @@ class LndRestWallet(Wallet):
|
||||
else:
|
||||
data["memo"] = memo or ""
|
||||
|
||||
r = post(
|
||||
r = httpx.post(
|
||||
url=f"{self.endpoint}/v1/invoices",
|
||||
headers=self.auth_invoice,
|
||||
verify=self.auth_cert,
|
||||
json=data,
|
||||
)
|
||||
|
||||
ok, checking_id, payment_request, error_message = r.ok, None, None, None
|
||||
if r.is_error:
|
||||
error_message = r.text
|
||||
try:
|
||||
error_message = r.json()["error"]
|
||||
except Exception:
|
||||
pass
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
if r.ok:
|
||||
data = r.json()
|
||||
payment_request = data["payment_request"]
|
||||
data = r.json()
|
||||
payment_request = data["payment_request"]
|
||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||
checking_id = payment_hash
|
||||
|
||||
r = get(
|
||||
url=f"{self.endpoint}/v1/payreq/{payment_request}",
|
||||
headers=self.auth_read,
|
||||
verify=self.auth_cert,
|
||||
)
|
||||
print(r)
|
||||
if r.ok:
|
||||
checking_id = r.json()["payment_hash"].replace("/", "_")
|
||||
print(checking_id)
|
||||
error_message = None
|
||||
ok = True
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(
|
||||
r = httpx.post(
|
||||
url=f"{self.endpoint}/v1/channels/transactions",
|
||||
headers=self.auth_admin,
|
||||
verify=self.auth_cert,
|
||||
json={"payment_request": bolt11},
|
||||
)
|
||||
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
|
||||
r = get(
|
||||
url=f"{self.endpoint}/v1/payreq/{bolt11}",
|
||||
headers=self.auth_admin,
|
||||
verify=self.auth_cert,
|
||||
)
|
||||
|
||||
if r.ok:
|
||||
checking_id = r.json()["payment_hash"]
|
||||
else:
|
||||
error_message = r.json()["error"]
|
||||
if r.is_error:
|
||||
error_message = r.text
|
||||
try:
|
||||
error_message = r.json()["error"]
|
||||
except:
|
||||
pass
|
||||
return PaymentResponse(False, None, 0, error_message)
|
||||
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
payment_hash = r.json()["payment_hash"]
|
||||
checking_id = payment_hash
|
||||
|
||||
return PaymentResponse(True, checking_id, 0, None)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
checking_id = checking_id.replace("_", "/")
|
||||
print(checking_id)
|
||||
r = get(
|
||||
r = httpx.get(
|
||||
url=f"{self.endpoint}/v1/invoice/{checking_id}",
|
||||
headers=self.auth_invoice,
|
||||
verify=self.auth_cert,
|
||||
)
|
||||
print(r.json()["settled"])
|
||||
if not r or r.json()["settled"] == False:
|
||||
return PaymentStatus(None)
|
||||
|
||||
return PaymentStatus(r.json()["settled"])
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(
|
||||
r = httpx.get(
|
||||
url=f"{self.endpoint}/v1/payments",
|
||||
headers=self.auth_admin,
|
||||
verify=self.auth_cert,
|
||||
params={"include_incomplete": "True", "max_payments": "20"},
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id]
|
||||
print(checking_id)
|
||||
payment = payments[0] if payments else None
|
||||
|
||||
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
|
||||
# check payment.status:
|
||||
# https://api.lightning.community/rest/index.html?python#peersynctype
|
||||
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
|
||||
|
||||
return PaymentStatus(statuses[payment["status"]])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = self.endpoint + "/v1/invoices/subscribe"
|
||||
|
||||
async with httpx.AsyncClient(timeout=None, headers=self.auth_admin, verify=self.auth_cert) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import json
|
||||
import trio # type: ignore
|
||||
import httpx
|
||||
from os import getenv
|
||||
from typing import Optional, Dict
|
||||
from requests import get, post
|
||||
from http import HTTPStatus
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
from quart import request
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
||||
@@ -9,15 +13,16 @@ class LNPayWallet(Wallet):
|
||||
"""https://docs.lnpay.co/"""
|
||||
|
||||
def __init__(self):
|
||||
endpoint = getenv("LNPAY_API_ENDPOINT")
|
||||
endpoint = getenv("LNPAY_API_ENDPOINT", "https://lnpay.co/v1")
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth_admin = getenv("LNPAY_ADMIN_KEY")
|
||||
self.auth_invoice = getenv("LNPAY_INVOICE_KEY")
|
||||
self.auth_read = getenv("LNPAY_READ_KEY")
|
||||
self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
|
||||
|
||||
def create_invoice(
|
||||
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
) -> InvoiceResponse:
|
||||
data: Dict = {"num_satoshis": f"{amount}"}
|
||||
if description_hash:
|
||||
@@ -25,12 +30,17 @@ class LNPayWallet(Wallet):
|
||||
else:
|
||||
data["memo"] = memo or ""
|
||||
|
||||
r = post(
|
||||
url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice",
|
||||
r = httpx.post(
|
||||
url=f"{self.endpoint}/user/wallet/{self.auth_admin}/invoice",
|
||||
headers=self.auth_api,
|
||||
json=data,
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, r.text
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
r.status_code == 201,
|
||||
None,
|
||||
None,
|
||||
r.text,
|
||||
)
|
||||
|
||||
if ok:
|
||||
data = r.json()
|
||||
@@ -39,7 +49,7 @@ class LNPayWallet(Wallet):
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(
|
||||
r = httpx.post(
|
||||
url=f"{self.endpoint}/user/wallet/{self.auth_admin}/withdraw",
|
||||
headers=self.auth_api,
|
||||
json={"payment_request": bolt11},
|
||||
@@ -55,10 +65,36 @@ class LNPayWallet(Wallet):
|
||||
return self.get_payment_status(checking_id)
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(url=f"{self.endpoint}/user/lntx/{checking_id}", headers=self.auth_api)
|
||||
r = httpx.get(
|
||||
url=f"{self.endpoint}/user/lntx/{checking_id}?fields=settled",
|
||||
headers=self.auth_api,
|
||||
)
|
||||
|
||||
if not r.ok:
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
statuses = {0: None, 1: True, -1: False}
|
||||
return PaymentStatus(statuses[r.json()["settled"]])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.send, receive = trio.open_memory_channel(0)
|
||||
async for value in receive:
|
||||
yield value
|
||||
|
||||
async def webhook_listener(self):
|
||||
text: str = await request.get_data()
|
||||
data = json.loads(text)
|
||||
if type(data) is not dict or "event" not in data or data["event"].get("name") != "wallet_receive":
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
lntx_id = data["data"]["wtx"]["lnTx"]["id"]
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/user/lntx/{lntx_id}?fields=settled",
|
||||
headers=self.auth_api,
|
||||
)
|
||||
data = r.json()
|
||||
if data["settled"]:
|
||||
await self.send.send(lntx_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import trio # type: ignore
|
||||
from os import getenv
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, AsyncGenerator
|
||||
from requests import post
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
@@ -75,3 +76,8 @@ class LntxbotWallet(Wallet):
|
||||
|
||||
statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
|
||||
return PaymentStatus(statuses[r.json().get("status", "unknown")])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
print("lntxbot does not support paid invoices stream yet")
|
||||
await trio.sleep(5)
|
||||
yield ""
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import json
|
||||
import trio # type: ignore
|
||||
import hmac
|
||||
import httpx
|
||||
from http import HTTPStatus
|
||||
from os import getenv
|
||||
from typing import Optional
|
||||
from requests import get, post
|
||||
from typing import Optional, AsyncGenerator
|
||||
from quart import request, url_for
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
||||
|
||||
@@ -20,47 +25,77 @@ class OpenNodeWallet(Wallet):
|
||||
if description_hash:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
r = post(
|
||||
url=f"{self.endpoint}/v1/charges",
|
||||
r = httpx.post(
|
||||
f"{self.endpoint}/v1/charges",
|
||||
headers=self.auth_invoice,
|
||||
json={"amount": f"{amount}", "description": memo}, # , "private": True},
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
"callback_url": url_for("webhook_listener", _external=True),
|
||||
},
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = r.ok, None, None, None
|
||||
|
||||
if r.ok:
|
||||
data = r.json()["data"]
|
||||
checking_id, payment_request = data["id"], data["lightning_invoice"]["payreq"]
|
||||
else:
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
payment_request = data["lightning_invoice"]["payreq"]
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
def pay_invoice(self, bolt11: str) -> PaymentResponse:
|
||||
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11})
|
||||
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
|
||||
r = httpx.post(
|
||||
f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11}
|
||||
)
|
||||
|
||||
if r.ok:
|
||||
data = r.json()["data"]
|
||||
checking_id, fee_msat = data["id"], data["fee"] * 1000
|
||||
else:
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return PaymentResponse(False, None, 0, error_message)
|
||||
|
||||
return PaymentResponse(ok, checking_id, fee_msat, error_message)
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
fee_msat = data["fee"] * 1000
|
||||
return PaymentResponse(True, checking_id, fee_msat, None)
|
||||
|
||||
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(url=f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
|
||||
r = httpx.get(f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
|
||||
|
||||
if not r.ok:
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
statuses = {"processing": None, "paid": True, "unpaid": False}
|
||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = get(url=f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
|
||||
r = httpx.get(f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
|
||||
|
||||
if not r.ok:
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False}
|
||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.send, receive = trio.open_memory_channel(0)
|
||||
async for value in receive:
|
||||
yield value
|
||||
|
||||
async def webhook_listener(self):
|
||||
text: str = await request.get_data()
|
||||
data = json.loads(text)
|
||||
if type(data) is not dict or "event" not in data or data["event"].get("name") != "wallet_receive":
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
charge_id = data["id"]
|
||||
if data["status"] != "paid":
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
x = hmac.new(self.auth_invoice["Authorization"], digestmod="sha256")
|
||||
x.update(charge_id)
|
||||
if x.hexdigest() != data["hashed_order"]:
|
||||
print("invalid webhook, not from opennode")
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
await self.send.send(charge_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
import httpx
|
||||
from os import getenv
|
||||
from typing import Optional
|
||||
from typing import Optional, AsyncGenerator
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
|
||||
|
||||
@@ -16,7 +17,7 @@ class UnknownError(Exception):
|
||||
|
||||
class SparkWallet(Wallet):
|
||||
def __init__(self):
|
||||
self.url = getenv("SPARK_URL")
|
||||
self.url = getenv("SPARK_URL").replace("/rpc", "")
|
||||
self.token = getenv("SPARK_TOKEN")
|
||||
|
||||
def __getattr__(self, key):
|
||||
@@ -28,12 +29,12 @@ class SparkWallet(Wallet):
|
||||
elif kwargs:
|
||||
params = kwargs
|
||||
|
||||
r = requests.post(self.url, headers={"X-Access": self.token}, json={"method": key, "params": params})
|
||||
r = httpx.post(self.url + "/rpc", headers={"X-Access": self.token}, json={"method": key, "params": params})
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
raise UnknownError(r.text)
|
||||
if not r.ok:
|
||||
if r.is_error:
|
||||
raise SparkError(data["message"])
|
||||
return data
|
||||
|
||||
@@ -91,3 +92,14 @@ class SparkWallet(Wallet):
|
||||
return PaymentStatus(False)
|
||||
return PaymentStatus(None)
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = self.url + "/stream?access-key=" + self.token
|
||||
|
||||
async with httpx.AsyncClient(timeout=None) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
data = json.loads(line[5:])
|
||||
if "pay_index" in data and data.get("status") == "paid":
|
||||
yield data["label"]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, AsyncGenerator
|
||||
|
||||
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
|
||||
|
||||
@@ -17,3 +17,6 @@ class VoidWallet(Wallet):
|
||||
|
||||
def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
raise Unsupported("")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
yield ""
|
||||
|
2
pytest.ini
Normal file
2
pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
trio_mode = true
|
@@ -1,4 +1,6 @@
|
||||
aiofiles==0.5.0
|
||||
async-generator==1.10
|
||||
attrs==20.2.0
|
||||
bech32==1.2.0
|
||||
bitstring==3.1.7
|
||||
blinker==1.4
|
||||
@@ -13,7 +15,7 @@ h11==0.9.0
|
||||
h2==4.0.0
|
||||
hpack==4.0.0
|
||||
httpcore==0.11.1
|
||||
httpx==0.15.4
|
||||
httpx==0.15.5
|
||||
hypercorn==0.11.0
|
||||
hyperframe==6.0.0
|
||||
idna==2.10
|
||||
@@ -22,6 +24,7 @@ jinja2==2.11.2
|
||||
lnurl==0.3.5
|
||||
markupsafe==1.1.1
|
||||
marshmallow==3.8.0
|
||||
outcome==1.0.1
|
||||
priority==1.3.0
|
||||
pydantic==1.6.1
|
||||
pyscss==1.3.7
|
||||
@@ -29,13 +32,16 @@ python-dotenv==0.14.0
|
||||
quart==0.13.1
|
||||
quart-compress==0.2.1
|
||||
quart-cors==0.3.0
|
||||
quart-trio==0.5.1
|
||||
requests==2.24.0
|
||||
rfc3986==1.4.0
|
||||
secure==0.2.1
|
||||
shortuuid==1.0.1
|
||||
six==1.15.0
|
||||
sniffio==1.1.0
|
||||
sortedcontainers==2.2.2
|
||||
toml==0.10.1
|
||||
trio==0.17.0
|
||||
typing-extensions==3.7.4.3
|
||||
urllib3==1.25.10
|
||||
werkzeug==1.0.1
|
||||
|
@@ -4,7 +4,6 @@ from lnbits.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def client():
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_homepage(client):
|
||||
r = await client.get("/")
|
||||
assert b"Add a new wallet" in await r.get_data()
|
||||
|
Reference in New Issue
Block a user