Merge pull request #171 from lnbits/satspay
Adds WatchOnly wallet and SatsPayServer extensions
This commit is contained in:
commit
9fa05e43f9
1
Pipfile
1
Pipfile
|
@ -24,6 +24,7 @@ quart-trio = "*"
|
|||
trio = "==0.16.0"
|
||||
hypercorn = {extras = ["trio"], version = "*"}
|
||||
sqlalchemy-aio = "*"
|
||||
embit = "*"
|
||||
pyqrcode = "*"
|
||||
pypng = "*"
|
||||
|
||||
|
|
191
Pipfile.lock
generated
191
Pipfile.lock
generated
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
<<<<<<< HEAD
|
||||
"sha256": "9e5dd461dc1a7d645f089c7e7a67fb7bfaf47273eaf2ebadbdb60234bfb34710"
|
||||
=======
|
||||
"sha256": "f98f5cc03179f57291aeeca8e0e117ef4f38806176c9d2c0f984f501a5806338"
|
||||
>>>>>>> master
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -28,7 +32,6 @@
|
|||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
|
@ -36,7 +39,6 @@
|
|||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"bech32": {
|
||||
|
@ -44,7 +46,6 @@
|
|||
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
|
||||
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"bitstring": {
|
||||
|
@ -116,7 +117,6 @@
|
|||
"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": {
|
||||
|
@ -127,6 +127,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
},
|
||||
"embit": {
|
||||
"hashes": [
|
||||
"sha256:d88e74eafdd26280c298bb572ac28e7913abbc2c91e99df0ebb9139c626bc4d3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.1.2"
|
||||
},
|
||||
"environs": {
|
||||
"hashes": [
|
||||
"sha256:2da44b7c30114415aa858577fa6396ee326fc76a0a60f0f15e8260ba554f19dc",
|
||||
|
@ -197,7 +204,6 @@
|
|||
"sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25",
|
||||
"sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"hpack": {
|
||||
|
@ -205,7 +211,6 @@
|
|||
"sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c",
|
||||
"sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"httpcore": {
|
||||
|
@ -213,8 +218,12 @@
|
|||
"sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
|
||||
"sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"version": "==0.12.2"
|
||||
=======
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.3"
|
||||
>>>>>>> master
|
||||
},
|
||||
"httpx": {
|
||||
"hashes": [
|
||||
|
@ -225,9 +234,6 @@
|
|||
"version": "==0.17.1"
|
||||
},
|
||||
"hypercorn": {
|
||||
"extras": [
|
||||
"trio"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a",
|
||||
"sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"
|
||||
|
@ -240,7 +246,6 @@
|
|||
"sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1",
|
||||
"sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"idna": {
|
||||
|
@ -255,7 +260,6 @@
|
|||
"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": {
|
||||
|
@ -263,8 +267,12 @@
|
|||
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
|
||||
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"version": "==2.11.2"
|
||||
=======
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.3"
|
||||
>>>>>>> master
|
||||
},
|
||||
"lnurl": {
|
||||
"hashes": [
|
||||
|
@ -328,7 +336,6 @@
|
|||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
|
||||
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"marshmallow": {
|
||||
|
@ -336,15 +343,18 @@
|
|||
"sha256:4ab2fdb7f36eb61c3665da67a7ce281c8900db08d72ba6bf0e695828253581f7",
|
||||
"sha256:eca81d53aa4aafbc0e20566973d0d2e50ce8bf0ee15165bb799bec0df1e50177"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"version": "==3.9.1"
|
||||
=======
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.10.0"
|
||||
>>>>>>> master
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"priority": {
|
||||
|
@ -356,6 +366,32 @@
|
|||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f",
|
||||
"sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef",
|
||||
"sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9",
|
||||
"sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec",
|
||||
"sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229",
|
||||
"sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af",
|
||||
"sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e",
|
||||
"sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f",
|
||||
"sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1",
|
||||
"sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2",
|
||||
"sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b",
|
||||
"sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009",
|
||||
"sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c",
|
||||
"sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd",
|
||||
"sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e",
|
||||
"sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127",
|
||||
"sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23",
|
||||
"sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef",
|
||||
"sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730",
|
||||
"sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608",
|
||||
"sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e",
|
||||
"sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"
|
||||
],
|
||||
"version": "==1.7.3"
|
||||
=======
|
||||
"sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850",
|
||||
"sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f",
|
||||
"sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683",
|
||||
|
@ -396,6 +432,7 @@
|
|||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.1"
|
||||
>>>>>>> master
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
|
@ -452,9 +489,6 @@
|
|||
"version": "==1.6.0.post0"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
|
@ -482,7 +516,10 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
>>>>>>> master
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sniffio": {
|
||||
|
@ -490,7 +527,6 @@
|
|||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
|
@ -502,6 +538,48 @@
|
|||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
|
||||
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
|
||||
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
|
||||
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
|
||||
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
|
||||
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
|
||||
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
|
||||
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
|
||||
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
|
||||
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
|
||||
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
|
||||
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
|
||||
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
|
||||
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
|
||||
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
|
||||
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
|
||||
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
|
||||
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
|
||||
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
|
||||
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
|
||||
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
|
||||
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
|
||||
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
|
||||
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
|
||||
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
|
||||
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
|
||||
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
|
||||
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
|
||||
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
|
||||
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
|
||||
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
|
||||
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
|
||||
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
|
||||
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
|
||||
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
|
||||
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
|
||||
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
|
||||
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
|
||||
],
|
||||
"version": "==1.3.20"
|
||||
=======
|
||||
"sha256:0096305b3e0912c59d8308f55d17544b3e5c1787f5ad8ef9cd75084136bcba9c",
|
||||
"sha256:106fd3da313390dffe6ca156e5b7244293d6f4bfd389bcb315771c7addb5f3b3",
|
||||
"sha256:1b30b71ea7c0f854d1b31549816694d8c435c9b5cce44da140b473544bb48a6b",
|
||||
|
@ -539,6 +617,7 @@
|
|||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.4.3"
|
||||
>>>>>>> master
|
||||
},
|
||||
"sqlalchemy-aio": {
|
||||
"hashes": [
|
||||
|
@ -553,7 +632,10 @@
|
|||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
>>>>>>> master
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"trio": {
|
||||
|
@ -578,7 +660,6 @@
|
|||
"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": {
|
||||
|
@ -586,7 +667,6 @@
|
|||
"sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38",
|
||||
"sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -603,7 +683,6 @@
|
|||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
|
@ -611,7 +690,6 @@
|
|||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"black": {
|
||||
|
@ -626,7 +704,6 @@
|
|||
"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": {
|
||||
|
@ -684,8 +761,12 @@
|
|||
"sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
|
||||
"sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"version": "==5.3"
|
||||
=======
|
||||
"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"
|
||||
>>>>>>> master
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
|
@ -741,16 +822,22 @@
|
|||
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
||||
],
|
||||
"version": "==20.7"
|
||||
=======
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.9"
|
||||
>>>>>>> master
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
|
@ -764,7 +851,6 @@
|
|||
"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": {
|
||||
|
@ -772,15 +858,22 @@
|
|||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"version": "==1.9.0"
|
||||
=======
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.10.0"
|
||||
>>>>>>> master
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
>>>>>>> master
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
|
@ -808,6 +901,51 @@
|
|||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538",
|
||||
"sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4",
|
||||
"sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc",
|
||||
"sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa",
|
||||
"sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444",
|
||||
"sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1",
|
||||
"sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af",
|
||||
"sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8",
|
||||
"sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9",
|
||||
"sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88",
|
||||
"sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba",
|
||||
"sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364",
|
||||
"sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e",
|
||||
"sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7",
|
||||
"sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0",
|
||||
"sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31",
|
||||
"sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683",
|
||||
"sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee",
|
||||
"sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b",
|
||||
"sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884",
|
||||
"sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c",
|
||||
"sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e",
|
||||
"sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562",
|
||||
"sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85",
|
||||
"sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c",
|
||||
"sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6",
|
||||
"sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d",
|
||||
"sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b",
|
||||
"sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70",
|
||||
"sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b",
|
||||
"sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b",
|
||||
"sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f",
|
||||
"sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0",
|
||||
"sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5",
|
||||
"sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5",
|
||||
"sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f",
|
||||
"sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e",
|
||||
"sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512",
|
||||
"sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d",
|
||||
"sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917",
|
||||
"sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"
|
||||
],
|
||||
"version": "==2020.11.13"
|
||||
=======
|
||||
"sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139",
|
||||
"sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5",
|
||||
"sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa",
|
||||
|
@ -851,13 +989,13 @@
|
|||
"sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"
|
||||
],
|
||||
"version": "==2021.3.17"
|
||||
>>>>>>> master
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
|
@ -872,7 +1010,10 @@
|
|||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
>>>>>>> master
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"trio": {
|
||||
|
|
4
lnbits/extensions/satspay/README.md
Normal file
4
lnbits/extensions/satspay/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# SatsPay Server
|
||||
|
||||
Create onchain and LN charges. Includes webhooks!
|
||||
|
13
lnbits/extensions/satspay/__init__.py
Normal file
13
lnbits/extensions/satspay/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_satspay")
|
||||
|
||||
|
||||
satspay_ext: Blueprint = Blueprint(
|
||||
"satspay", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
8
lnbits/extensions/satspay/config.json
Normal file
8
lnbits/extensions/satspay/config.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "SatsPayServer",
|
||||
"short_description": "Create onchain and LN charges",
|
||||
"icon": "payment",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
}
|
128
lnbits/extensions/satspay/crud.py
Normal file
128
lnbits/extensions/satspay/crud.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
# from lnbits.db import open_ext_db
|
||||
from . import db
|
||||
from .models import Charges
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from quart import jsonify
|
||||
import httpx
|
||||
from lnbits.core.services import create_invoice, check_invoice_status
|
||||
from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool
|
||||
|
||||
|
||||
###############CHARGES##########################
|
||||
|
||||
|
||||
async def create_charge(
|
||||
user: str,
|
||||
description: str = None,
|
||||
onchainwallet: Optional[str] = None,
|
||||
lnbitswallet: Optional[str] = None,
|
||||
webhook: Optional[str] = None,
|
||||
completelink: Optional[str] = None,
|
||||
completelinktext: Optional[str] = None,
|
||||
time: Optional[int] = None,
|
||||
amount: Optional[int] = None,
|
||||
) -> Charges:
|
||||
charge_id = urlsafe_short_hash()
|
||||
if onchainwallet:
|
||||
wallet = await get_watch_wallet(onchainwallet)
|
||||
onchain = await get_fresh_address(onchainwallet)
|
||||
onchainaddress = onchain.address
|
||||
else:
|
||||
onchainaddress = None
|
||||
if lnbitswallet:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=lnbitswallet, amount=amount, memo=charge_id
|
||||
)
|
||||
else:
|
||||
payment_hash = None
|
||||
payment_request = None
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO charges (
|
||||
id,
|
||||
user,
|
||||
description,
|
||||
onchainwallet,
|
||||
onchainaddress,
|
||||
lnbitswallet,
|
||||
payment_request,
|
||||
payment_hash,
|
||||
webhook,
|
||||
completelink,
|
||||
completelinktext,
|
||||
time,
|
||||
amount,
|
||||
balance
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
charge_id,
|
||||
user,
|
||||
description,
|
||||
onchainwallet,
|
||||
onchainaddress,
|
||||
lnbitswallet,
|
||||
payment_request,
|
||||
payment_hash,
|
||||
webhook,
|
||||
completelink,
|
||||
completelinktext,
|
||||
time,
|
||||
amount,
|
||||
0,
|
||||
),
|
||||
)
|
||||
return await get_charge(charge_id)
|
||||
|
||||
|
||||
async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM charges WHERE id = ?", (charge_id,))
|
||||
return Charges.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_charge(charge_id: str) -> Charges:
|
||||
row = await db.fetchone("SELECT * FROM charges WHERE id = ?", (charge_id,))
|
||||
return Charges.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_charges(user: str) -> List[Charges]:
|
||||
rows = await db.fetchall("SELECT * FROM charges WHERE user = ?", (user,))
|
||||
return [Charges.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_charge(charge_id: str) -> None:
|
||||
await db.execute("DELETE FROM charges WHERE id = ?", (charge_id,))
|
||||
|
||||
|
||||
async def check_address_balance(charge_id: str) -> List[Charges]:
|
||||
charge = await get_charge(charge_id)
|
||||
if not charge.paid:
|
||||
if charge.onchainaddress:
|
||||
mempool = await get_mempool(charge.user)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
mempool.endpoint + "/api/address/" + charge.onchainaddress
|
||||
)
|
||||
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
||||
if respAmount >= charge.balance:
|
||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
||||
except Exception:
|
||||
pass
|
||||
if charge.lnbitswallet:
|
||||
invoice_status = await check_invoice_status(
|
||||
charge.lnbitswallet, charge.payment_hash
|
||||
)
|
||||
if invoice_status.paid:
|
||||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
||||
row = await db.fetchone("SELECT * FROM charges WHERE id = ?", (charge_id,))
|
||||
return Charges.from_row(row) if row else None
|
26
lnbits/extensions/satspay/migrations.py
Normal file
26
lnbits/extensions/satspay/migrations.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial wallet table.
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS charges (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
user TEXT,
|
||||
description TEXT,
|
||||
onchainwallet TEXT,
|
||||
onchainaddress TEXT,
|
||||
lnbitswallet TEXT,
|
||||
payment_request TEXT,
|
||||
payment_hash TEXT,
|
||||
webhook TEXT,
|
||||
completelink TEXT,
|
||||
completelinktext TEXT,
|
||||
time INTEGER,
|
||||
amount INTEGER,
|
||||
balance INTEGER DEFAULT 0,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
39
lnbits/extensions/satspay/models.py
Normal file
39
lnbits/extensions/satspay/models.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from sqlite3 import Row
|
||||
from typing import NamedTuple
|
||||
import time
|
||||
|
||||
|
||||
class Charges(NamedTuple):
|
||||
id: str
|
||||
user: str
|
||||
description: str
|
||||
onchainwallet: str
|
||||
onchainaddress: str
|
||||
lnbitswallet: str
|
||||
payment_request: str
|
||||
payment_hash: str
|
||||
webhook: str
|
||||
completelink: str
|
||||
completelinktext: str
|
||||
time: int
|
||||
amount: int
|
||||
balance: int
|
||||
timestamp: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Charges":
|
||||
return cls(**dict(row))
|
||||
|
||||
@property
|
||||
def time_elapsed(self):
|
||||
if (self.timestamp + (self.time * 60)) >= time.time():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def paid(self):
|
||||
if self.balance >= self.amount:
|
||||
return True
|
||||
else:
|
||||
return False
|
171
lnbits/extensions/satspay/templates/satspay/_api_docs.html
Normal file
171
lnbits/extensions/satspay/templates/satspay/_api_docs.html
Normal file
|
@ -0,0 +1,171 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
SatsPayServer, create Onchain/LN charges.<br />WARNING: If using with the
|
||||
WatchOnly extension, we highly reccomend using a fresh extended public Key
|
||||
specifically for SatsPayServer!<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create charge">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">POST</span> /satspay/api/v1/charge</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<charge_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/charge -d
|
||||
'{"onchainwallet": <string, watchonly_wallet_id>,
|
||||
"description": <string>, "webhook":<string>, "time":
|
||||
<integer>, "amount": <integer>, "lnbitswallet":
|
||||
<string, lnbits_wallet_id>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Update charge">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">PUT</span>
|
||||
/satspay/api/v1/charge/<charge_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<charge_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/charge/<charge_id>
|
||||
-d '{"onchainwallet": <string, watchonly_wallet_id>,
|
||||
"description": <string>, "webhook":<string>, "time":
|
||||
<integer>, "amount": <integer>, "lnbitswallet":
|
||||
<string, lnbits_wallet_id>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="Get charge">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/satspay/api/v1/charge/<charge_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<charge_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/charge/<charge_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Get charges">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /satspay/api/v1/charges</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<charge_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a pay link"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/satspay/api/v1/charge/<charge_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}api/v1/charge/<charge_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Get balances">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/satspay/api/v1/charges/balance/<charge_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<charge_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
</q-card>
|
318
lnbits/extensions/satspay/templates/satspay/display.html
Normal file
318
lnbits/extensions/satspay/templates/satspay/display.html
Normal file
|
@ -0,0 +1,318 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="q-pa-sm theCard">
|
||||
<q-card class="my-card">
|
||||
<div class="column">
|
||||
<center>
|
||||
<div class="col theHeading">{{ charge.description }}</div>
|
||||
</center>
|
||||
<div class="col">
|
||||
<div
|
||||
class="col"
|
||||
color="white"
|
||||
style="background-color: grey; height: 30px; padding: 5px"
|
||||
v-if="charge_time_elapsed == 'True'"
|
||||
>
|
||||
<center>Time elapsed</center>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
color="white"
|
||||
style="background-color: grey; height: 30px; padding: 5px"
|
||||
v-else-if="charge_paid == 'True'"
|
||||
>
|
||||
<center>Charge paid</center>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-linear-progress size="30px" :value="newProgress" color="grey">
|
||||
<q-item-section>
|
||||
<q-item style="padding: 3px">
|
||||
<q-spinner color="white" size="0.8em"></q-spinner
|
||||
><span style="font-size: 15px; color: white"
|
||||
><span class="q-pr-xl q-pl-md"> Awaiting payment...</span>
|
||||
<span class="q-pl-xl" style="color: white">
|
||||
{% raw %} {{ newTimeLeft }} {% endraw %}</span
|
||||
></span
|
||||
>
|
||||
</q-item>
|
||||
</q-item-section>
|
||||
</q-linear-progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" style="margin: 2px 15px; max-height: 100px">
|
||||
<center>
|
||||
<q-btn flat dense outline @click="copyText('{{ charge.id }}')"
|
||||
>Charge ID: {{ charge.id }}</q-btn
|
||||
>
|
||||
</center>
|
||||
<span
|
||||
><small
|
||||
>{% raw %} Total to pay: {{ charge_amount }}sats<br />
|
||||
Amount paid: {{ charge_balance }}</small
|
||||
><br />
|
||||
Amount due: {{ charge_amount - charge_balance }}sats {% endraw %}
|
||||
</span>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="'{{ charge.lnbitswallet }}' == 'None' || charge_time_elapsed == 'True'"
|
||||
style="color: primary; width: 100%"
|
||||
label="lightning⚡"
|
||||
>
|
||||
<q-tooltip>
|
||||
bitcoin onchain payment method not available
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
v-else
|
||||
@click="payLN"
|
||||
style="color: primary; width: 100%"
|
||||
label="lightning⚡"
|
||||
>
|
||||
<q-tooltip> pay with lightning </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="'{{ charge.onchainwallet }}' == 'None' || charge_time_elapsed == 'True'"
|
||||
style="color: primary; width: 100%"
|
||||
label="onchain⛓️"
|
||||
>
|
||||
<q-tooltip>
|
||||
bitcoin lightning payment method not available
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
v-else
|
||||
@click="payON"
|
||||
style="color: primary; width: 100%"
|
||||
label="onchain⛓️"
|
||||
>
|
||||
<q-tooltip> pay onchain </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
</div>
|
||||
</div>
|
||||
<q-card class="q-pa-lg" v-if="lnbtc">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center q-pt-md">
|
||||
<div v-if="charge_time_elapsed == 'True' && charge_paid == 'False'">
|
||||
<q-icon
|
||||
name="block"
|
||||
style="color: #ccc; font-size: 21.4em"
|
||||
></q-icon>
|
||||
</div>
|
||||
<div v-else-if="charge_paid == 'True'">
|
||||
<q-icon
|
||||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<q-btn
|
||||
outline
|
||||
v-if="'{{ charge.webhook }}' != 'None'"
|
||||
type="a"
|
||||
href="{{ charge.completelink }}"
|
||||
label="{{ charge.completelinktext }}"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<center>
|
||||
<span class="text-subtitle2"
|
||||
>Pay this <br />
|
||||
lightning-network invoice</span
|
||||
>
|
||||
</center>
|
||||
<a href="lightning://{{ charge.payment_request }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ charge.payment_request }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText('{{ charge.payment_request }}')"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-pa-lg" v-if="onbtc">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center q-pt-md">
|
||||
<div v-if="charge_time_elapsed == 'True' && charge_paid == 'False'">
|
||||
<q-icon
|
||||
name="block"
|
||||
style="color: #ccc; font-size: 21.4em"
|
||||
></q-icon>
|
||||
</div>
|
||||
<div v-else-if="charge_paid == 'True'">
|
||||
<q-icon
|
||||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<q-btn
|
||||
outline
|
||||
v-if="'{{ charge.webhook }}' != 'None'"
|
||||
type="a"
|
||||
href="{{ charge.completelink }}"
|
||||
label="{{ charge.completelinktext }}"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<center>
|
||||
<span class="text-subtitle2"
|
||||
>Send {{ charge.amount }}sats<br />
|
||||
to this onchain address</span
|
||||
>
|
||||
</center>
|
||||
<a href="bitcoin://{{ charge.onchainaddress }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ charge.onchainaddress }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText('{{ charge.onchainaddress }}')"
|
||||
>Copy address</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style>
|
||||
.theCard {
|
||||
width: 360px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
.theHeading {
|
||||
margin: 15px;
|
||||
font-size: 25px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
newProgress: 0.4,
|
||||
counter: 1,
|
||||
newTimeLeft: '',
|
||||
lnbtc: true,
|
||||
onbtc: false,
|
||||
charge_time_elapsed: '{{charge.time_elapsed}}',
|
||||
charge_amount: '{{charge.amount}}',
|
||||
charge_balance: '{{charge.balance}}',
|
||||
charge_paid: '{{charge.paid}}'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkBalance: function () {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/satspay/api/v1/charges/balance/{{ charge.id }}',
|
||||
'filla'
|
||||
)
|
||||
.then(function (response) {
|
||||
console.log(response.data)
|
||||
self.charge_time_elapsed = response.data.time_elapsed
|
||||
self.charge_amount = response.data.amount
|
||||
self.charge_balance = response.data.balance
|
||||
if (self.charge_balance >= self.charge_amount) {
|
||||
self.charge_paid = 'True'
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
payLN: function () {
|
||||
this.lnbtc = true
|
||||
this.onbtc = false
|
||||
},
|
||||
payON: function () {
|
||||
this.lnbtc = false
|
||||
this.onbtc = true
|
||||
},
|
||||
getTheTime: function () {
|
||||
var timeToComplete =
|
||||
parseInt('{{ charge.time }}') * 60 -
|
||||
(Date.now() / 1000 - parseInt('{{ charge.timestamp }}'))
|
||||
var timeLeft = Quasar.utils.date.formatDate(
|
||||
new Date((timeToComplete - 3600) * 1000),
|
||||
'HH:mm:ss'
|
||||
)
|
||||
this.newTimeLeft = timeLeft
|
||||
},
|
||||
getThePercentage: function () {
|
||||
var timeToComplete =
|
||||
parseInt('{{ charge.time }}') * 60 -
|
||||
(Date.now() / 1000 - parseInt('{{ charge.timestamp }}'))
|
||||
this.newProgress =
|
||||
1 - timeToComplete / (parseInt('{{ charge.time }}') * 60)
|
||||
},
|
||||
|
||||
timerCount: function () {
|
||||
self = this
|
||||
var refreshIntervalId = setInterval(function () {
|
||||
if (self.charge_paid == 'True') {
|
||||
console.log('did this work?')
|
||||
clearInterval(refreshIntervalId)
|
||||
}
|
||||
self.getTheTime()
|
||||
self.getThePercentage()
|
||||
self.counter++
|
||||
if (self.counter % 10 === 0) {
|
||||
self.checkBalance()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if ('{{ charge.lnbitswallet }}' == 'None') {
|
||||
this.lnbtc = false
|
||||
this.onbtc = true
|
||||
}
|
||||
this.getTheTime()
|
||||
this.getThePercentage()
|
||||
var timerCount = this.timerCount
|
||||
if ('{{ charge.paid }}' == 'False') {
|
||||
timerCount()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
556
lnbits/extensions/satspay/templates/satspay/index.html
Normal file
556
lnbits/extensions/satspay/templates/satspay/index.html
Normal file
|
@ -0,0 +1,556 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
@click="formDialogCharge.show = true"
|
||||
>New charge
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Charges</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-input
|
||||
borderless
|
||||
dense
|
||||
debounce="300"
|
||||
v-model="filter"
|
||||
placeholder="Search"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn flat color="grey" @click="exportchargeCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="ChargeLinks"
|
||||
row-key="id"
|
||||
:columns="ChargesTable.columns"
|
||||
:pagination.sync="ChargesTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.label }}</div>
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
>
|
||||
<q-tooltip> Payment link </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
v-if="props.row.time_elapsed && props.row.balance < props.row.amount"
|
||||
unelevated
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
icon="error"
|
||||
:color="($q.dark.isActive) ? 'red' : 'red'"
|
||||
>
|
||||
<q-tooltip> Time elapsed </q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-else-if="props.row.balance >= props.row.amount"
|
||||
unelevated
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
icon="check"
|
||||
:color="($q.dark.isActive) ? 'green' : 'green'"
|
||||
>
|
||||
<q-tooltip> PAID! </q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="cached"
|
||||
flat
|
||||
:color="($q.dark.isActive) ? 'blue' : 'blue'"
|
||||
>
|
||||
<q-tooltip> Processing </q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteChargeLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete charge </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.value }}</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits satspay Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "satspay/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-dialog
|
||||
v-model="formDialogCharge.show"
|
||||
position="top"
|
||||
@hide="closeFormDialog"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormDataCharge" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.description"
|
||||
type="text"
|
||||
label="*Description"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.amount"
|
||||
type="number"
|
||||
label="*Amount (sats)"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.time"
|
||||
type="number"
|
||||
max="1440"
|
||||
label="*Mins valid for (max 1440)"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.webhook"
|
||||
type="url"
|
||||
label="Webhook (URL to send transaction data to once paid)"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelink"
|
||||
type="url"
|
||||
label="Completed button URL"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelinktext"
|
||||
type="text"
|
||||
label="Completed button text (ie 'Back to merchant')"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="walletLinks.length > 0">
|
||||
<q-checkbox
|
||||
v-model="formDialogCharge.data.onchain"
|
||||
label="Onchain"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-checkbox :value="false" label="Onchain" disabled>
|
||||
<q-tooltip>
|
||||
Watch-Only extension MUST be activated and have a wallet
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>
|
||||
<q-checkbox
|
||||
v-model="formDialogCharge.data.lnbits"
|
||||
label="LNbits wallet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formDialogCharge.data.onchain">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialogCharge.data.onchainwallet"
|
||||
:options="walletLinks"
|
||||
label="Onchain Wallet"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
v-if="formDialogCharge.data.lnbits"
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialogCharge.data.lnbitswallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="
|
||||
formDialogCharge.data.time == null ||
|
||||
formDialogCharge.data.amount == null"
|
||||
type="submit"
|
||||
>Create Charge</q-btn
|
||||
>
|
||||
<q-btn @click="cancelCharge" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style></style>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var mapCharge = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||
obj.time = obj.time + 'mins'
|
||||
|
||||
if (obj.time_elapsed) {
|
||||
obj.date = 'Time elapsed'
|
||||
} else {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date((obj.theTime - 3600) * 1000),
|
||||
'HH:mm:ss'
|
||||
)
|
||||
}
|
||||
obj.displayUrl = ['/satspay/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
filter: '',
|
||||
watchonlyactive: false,
|
||||
balance: null,
|
||||
checker: null,
|
||||
walletLinks: [],
|
||||
ChargeLinks: [],
|
||||
ChargeLinksObj: [],
|
||||
onchainwallet: '',
|
||||
currentaddress: '',
|
||||
Addresses: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
mempool: {
|
||||
endpoint: ''
|
||||
},
|
||||
|
||||
ChargesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'theId',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Title',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'timeleft',
|
||||
align: 'left',
|
||||
label: 'Time left',
|
||||
field: 'date'
|
||||
},
|
||||
{
|
||||
name: 'time to pay',
|
||||
align: 'left',
|
||||
label: 'Time to Pay',
|
||||
field: 'time'
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount to pay',
|
||||
field: 'amount'
|
||||
},
|
||||
{
|
||||
name: 'balance',
|
||||
align: 'left',
|
||||
label: 'Balance',
|
||||
field: 'balance'
|
||||
},
|
||||
{
|
||||
name: 'onchain address',
|
||||
align: 'left',
|
||||
label: 'Onchain Address',
|
||||
field: 'onchainaddress'
|
||||
},
|
||||
{
|
||||
name: 'LNbits wallet',
|
||||
align: 'left',
|
||||
label: 'LNbits wallet',
|
||||
field: 'lnbitswallet'
|
||||
},
|
||||
{
|
||||
name: 'Webhook link',
|
||||
align: 'left',
|
||||
label: 'Webhook link',
|
||||
field: 'webhook'
|
||||
},
|
||||
{
|
||||
name: 'Paid link',
|
||||
align: 'left',
|
||||
label: 'Paid link',
|
||||
field: 'completelink'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
formDialogCharge: {
|
||||
show: false,
|
||||
data: {
|
||||
onchain: false,
|
||||
lnbits: false,
|
||||
description: '',
|
||||
time: null,
|
||||
amount: null
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCharge: function (data) {
|
||||
var self = this
|
||||
self.formDialogCharge.data.description = ''
|
||||
self.formDialogCharge.data.onchainwallet = ''
|
||||
self.formDialogCharge.data.lnbitswallet = ''
|
||||
self.formDialogCharge.data.time = null
|
||||
self.formDialogCharge.data.amount = null
|
||||
self.formDialogCharge.data.webhook = ''
|
||||
self.formDialogCharge.data.completelink = ''
|
||||
self.formDialogCharge.show = false
|
||||
},
|
||||
|
||||
getWalletLinks: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/wallet',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
for (i = 0; i < response.data.length; i++) {
|
||||
self.walletLinks.push(response.data[i].id)
|
||||
}
|
||||
return
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
openQrCodeDialog: function (linkId) {
|
||||
var self = this
|
||||
var getAddresses = this.getAddresses
|
||||
getAddresses(linkId)
|
||||
self.current = linkId
|
||||
self.Addresses.show = true
|
||||
},
|
||||
getCharges: function () {
|
||||
var self = this
|
||||
var getAddressBalance = this.getAddressBalance
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/satspay/api/v1/charges',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.ChargeLinks = response.data.map(mapCharge)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
var self = this
|
||||
var wallet = this.g.user.wallets[0].adminkey
|
||||
var data = this.formDialogCharge.data
|
||||
data.amount = parseInt(data.amount)
|
||||
data.time = parseInt(data.time)
|
||||
this.createCharge(wallet, data)
|
||||
},
|
||||
timerCount: function () {
|
||||
self = this
|
||||
var refreshIntervalId = setInterval(function () {
|
||||
for (i = 0; i < self.ChargeLinks.length - 1; i++) {
|
||||
if (self.ChargeLinks[i]['paid'] == 'True') {
|
||||
setTimeout(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/satspay/api/v1/charges/balance/' +
|
||||
self.ChargeLinks[i]['id'],
|
||||
'filla'
|
||||
)
|
||||
.then(function (response) {})
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
self.getCharges()
|
||||
}, 20000)
|
||||
},
|
||||
createCharge: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('POST', '/satspay/api/v1/charge', wallet, data)
|
||||
.then(function (response) {
|
||||
self.ChargeLinks.push(mapCharge(response.data))
|
||||
self.formDialogCharge.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
|
||||
deleteChargeLink: function (chargeId) {
|
||||
var self = this
|
||||
var link = _.findWhere(this.ChargeLinks, {id: chargeId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/satspay/api/v1/charge/' + chargeId,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.ChargeLinks = _.reject(self.ChargeLinks, function (obj) {
|
||||
return obj.id === chargeId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportchargeCSV: function () {
|
||||
var self = this
|
||||
LNbits.utils.exportCSV(self.ChargesTable.columns, this.ChargeLinks)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
var getCharges = this.getCharges
|
||||
getCharges()
|
||||
var getWalletLinks = this.getWalletLinks
|
||||
getWalletLinks()
|
||||
var timerCount = this.timerCount
|
||||
timerCount()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
22
lnbits/extensions/satspay/views.py
Normal file
22
lnbits/extensions/satspay/views.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from quart import g, abort, render_template, jsonify
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from . import satspay_ext
|
||||
from .crud import get_charge
|
||||
|
||||
|
||||
@satspay_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("satspay/index.html", user=g.user)
|
||||
|
||||
|
||||
@satspay_ext.route("/<charge_id>")
|
||||
async def display(charge_id):
|
||||
charge = await get_charge(charge_id) or abort(
|
||||
HTTPStatus.NOT_FOUND, "Charge link does not exist."
|
||||
)
|
||||
return await render_template("satspay/display.html", charge=charge)
|
157
lnbits/extensions/satspay/views_api.py
Normal file
157
lnbits/extensions/satspay/views_api.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
import hashlib
|
||||
from quart import g, jsonify, url_for
|
||||
from http import HTTPStatus
|
||||
import httpx
|
||||
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from lnbits.extensions.satspay import satspay_ext
|
||||
from .crud import (
|
||||
create_charge,
|
||||
update_charge,
|
||||
get_charge,
|
||||
get_charges,
|
||||
delete_charge,
|
||||
check_address_balance,
|
||||
)
|
||||
|
||||
#############################CHARGES##########################
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/charge", methods=["POST"])
|
||||
@satspay_ext.route("/api/v1/charge/<charge_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"onchainwallet": {"type": "string"},
|
||||
"lnbitswallet": {"type": "string"},
|
||||
"description": {"type": "string", "empty": False, "required": True},
|
||||
"webhook": {"type": "string"},
|
||||
"completelink": {"type": "string"},
|
||||
"completelinktext": {"type": "string"},
|
||||
"time": {"type": "integer", "min": 1, "required": True},
|
||||
"amount": {"type": "integer", "min": 1, "required": True},
|
||||
}
|
||||
)
|
||||
async def api_charge_create_or_update(charge_id=None):
|
||||
if not charge_id:
|
||||
charge = await create_charge(user=g.wallet.user, **g.data)
|
||||
return jsonify(charge._asdict()), HTTPStatus.CREATED
|
||||
else:
|
||||
charge = await update_charge(charge_id=charge_id, **g.data)
|
||||
return jsonify(charge._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/charges", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_charges_retrieve():
|
||||
try:
|
||||
return (
|
||||
jsonify(
|
||||
[
|
||||
{
|
||||
**charge._asdict(),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
for charge in await get_charges(g.wallet.user)
|
||||
]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/charge/<charge_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_charge_retrieve(charge_id):
|
||||
charge = await get_charge(charge_id)
|
||||
|
||||
if not charge:
|
||||
return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
**charge._asdict(),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/charge/<charge_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_charge_delete(charge_id):
|
||||
charge = await get_charge(charge_id)
|
||||
|
||||
if not charge:
|
||||
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
await delete_charge(charge_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#############################BALANCE##########################
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/charges/balance/<charge_id>", methods=["GET"])
|
||||
async def api_charges_balance(charge_id):
|
||||
|
||||
charge = await check_address_balance(charge_id)
|
||||
|
||||
if not charge:
|
||||
return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND
|
||||
if charge.paid and charge.webhook:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
charge.webhook,
|
||||
json={
|
||||
"id": charge.id,
|
||||
"description": charge.description,
|
||||
"onchainaddress": charge.onchainaddress,
|
||||
"payment_request": charge.payment_request,
|
||||
"payment_hash": charge.payment_hash,
|
||||
"time": charge.time,
|
||||
"amount": charge.amount,
|
||||
"balance": charge.balance,
|
||||
"paid": charge.paid,
|
||||
"timestamp": charge.timestamp,
|
||||
"completelink": charge.completelink,
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
except AssertionError:
|
||||
charge.webhook = None
|
||||
return jsonify(charge._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
#############################MEMPOOL##########################
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/mempool", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"endpoint": {"type": "string", "empty": False, "required": True},
|
||||
}
|
||||
)
|
||||
async def api_update_mempool():
|
||||
mempool = await update_mempool(user=g.wallet.user, **g.data)
|
||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@satspay_ext.route("/api/v1/mempool", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_get_mempool():
|
||||
mempool = await get_mempool(g.wallet.user)
|
||||
if not mempool:
|
||||
mempool = await create_mempool(user=g.wallet.user)
|
||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
4
lnbits/extensions/watchonly/README.md
Normal file
4
lnbits/extensions/watchonly/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Watch Only wallet
|
||||
|
||||
Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API.
|
||||
|
13
lnbits/extensions/watchonly/__init__.py
Normal file
13
lnbits/extensions/watchonly/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_watchonly")
|
||||
|
||||
|
||||
watchonly_ext: Blueprint = Blueprint(
|
||||
"watchonly", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
8
lnbits/extensions/watchonly/config.json
Normal file
8
lnbits/extensions/watchonly/config.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Watch Only",
|
||||
"short_description": "Onchain watch only wallets",
|
||||
"icon": "visibility",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
}
|
199
lnbits/extensions/watchonly/crud.py
Normal file
199
lnbits/extensions/watchonly/crud.py
Normal file
|
@ -0,0 +1,199 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
# from lnbits.db import open_ext_db
|
||||
from . import db
|
||||
from .models import Wallets, Addresses, Mempool
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from embit.descriptor import Descriptor, Key
|
||||
from embit.descriptor.arguments import AllowedDerivation
|
||||
from embit.networks import NETWORKS
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
##########################WALLETS####################
|
||||
|
||||
|
||||
def detect_network(k):
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]:
|
||||
return net
|
||||
|
||||
|
||||
def parse_key(masterpub: str):
|
||||
"""Parses masterpub or descriptor and returns a tuple: (Descriptor, network)
|
||||
To create addresses use descriptor.derive(num).address(network=network)
|
||||
"""
|
||||
network = None
|
||||
# probably a single key
|
||||
if "(" not in masterpub:
|
||||
k = Key.from_string(masterpub)
|
||||
if not k.is_extended:
|
||||
raise ValueError("The key is not a master public key")
|
||||
if k.is_private:
|
||||
raise ValueError("Private keys are not allowed")
|
||||
# check depth
|
||||
if k.key.depth != 3:
|
||||
raise ValueError(
|
||||
"Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors."
|
||||
)
|
||||
# if allowed derivation is not provided use default /{0,1}/*
|
||||
if k.allowed_derivation is None:
|
||||
k.allowed_derivation = AllowedDerivation.default()
|
||||
# get version bytes
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"]]:
|
||||
network = net
|
||||
if version == net["xpub"]:
|
||||
desc = Descriptor.from_string("pkh(%s)" % str(k))
|
||||
elif version == net["ypub"]:
|
||||
desc = Descriptor.from_string("sh(wpkh(%s))" % str(k))
|
||||
elif version == net["zpub"]:
|
||||
desc = Descriptor.from_string("wpkh(%s)" % str(k))
|
||||
break
|
||||
# we didn't find correct version
|
||||
if network is None:
|
||||
raise ValueError("Unknown master public key version")
|
||||
else:
|
||||
desc = Descriptor.from_string(masterpub)
|
||||
if not desc.is_wildcard:
|
||||
raise ValueError("Descriptor should have wildcards")
|
||||
for k in desc.keys:
|
||||
if k.is_extended:
|
||||
net = detect_network(k)
|
||||
if net is None:
|
||||
raise ValueError(f"Unknown version: {k}")
|
||||
if network is not None and network != net:
|
||||
raise ValueError("Keys from different networks")
|
||||
network = net
|
||||
return desc, network
|
||||
|
||||
|
||||
async def create_watch_wallet(*, user: str, masterpub: str, title: str) -> Wallets:
|
||||
# check the masterpub is fine, it will raise an exception if not
|
||||
parse_key(masterpub)
|
||||
wallet_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO wallets (
|
||||
id,
|
||||
user,
|
||||
masterpub,
|
||||
title,
|
||||
address_no,
|
||||
balance
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
# address_no is -1 so fresh address on empty wallet can get address with index 0
|
||||
(wallet_id, user, masterpub, title, -1, 0),
|
||||
)
|
||||
# weallet_id = db.cursor.lastrowid
|
||||
|
||||
return await get_watch_wallet(wallet_id)
|
||||
|
||||
|
||||
async def get_watch_wallet(wallet_id: str) -> Wallets:
|
||||
row = await db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
|
||||
return Wallets.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_watch_wallets(user: str) -> List[Wallets]:
|
||||
rows = await db.fetchall("SELECT * FROM wallets WHERE user = ?", (user,))
|
||||
return [Wallets(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(
|
||||
f"UPDATE wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
|
||||
return Wallets.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_watch_wallet(wallet_id: str) -> None:
|
||||
await db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,))
|
||||
|
||||
########################ADDRESSES#######################
|
||||
|
||||
|
||||
async def get_derive_address(wallet_id: str, num: int):
|
||||
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
key = wallet[2]
|
||||
desc, network = parse_key(key)
|
||||
return desc.derive(num).address(network=network)
|
||||
|
||||
|
||||
async def get_fresh_address(wallet_id: str) -> Addresses:
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
address = await get_derive_address(wallet_id, wallet[4] + 1)
|
||||
|
||||
await update_watch_wallet(wallet_id=wallet_id, address_no=wallet[4] + 1)
|
||||
masterpub_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO addresses (
|
||||
id,
|
||||
address,
|
||||
wallet,
|
||||
amount
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(masterpub_id, address, wallet_id, 0),
|
||||
)
|
||||
|
||||
return await get_address(address)
|
||||
|
||||
|
||||
async def get_address(address: str) -> Addresses:
|
||||
row = await db.fetchone("SELECT * FROM addresses WHERE address = ?", (address,))
|
||||
return Addresses.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_addresses(wallet_id: str) -> List[Addresses]:
|
||||
rows = await db.fetchall("SELECT * FROM addresses WHERE wallet = ?", (wallet_id,))
|
||||
return [Addresses(**row) for row in rows]
|
||||
|
||||
|
||||
######################MEMPOOL#######################
|
||||
|
||||
|
||||
async def create_mempool(user: str) -> Mempool:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO mempool (
|
||||
user,
|
||||
endpoint
|
||||
)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(user, "https://mempool.space"),
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,))
|
||||
return Mempool.from_row(row) if row else None
|
||||
|
||||
|
||||
async def update_mempool(user: str, **kwargs) -> Optional[Mempool]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(f"UPDATE mempool SET {q} WHERE user = ?", (*kwargs.values(), user))
|
||||
row = await db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,))
|
||||
return Mempool.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_mempool(user: str) -> Mempool:
|
||||
row = await db.fetchone("SELECT * FROM mempool WHERE user = ?", (user,))
|
||||
return Mempool.from_row(row) if row else None
|
36
lnbits/extensions/watchonly/migrations.py
Normal file
36
lnbits/extensions/watchonly/migrations.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial wallet table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
user TEXT,
|
||||
masterpub TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
address_no INTEGER NOT NULL DEFAULT 0,
|
||||
balance INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS addresses (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mempool (
|
||||
user TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
35
lnbits/extensions/watchonly/models.py
Normal file
35
lnbits/extensions/watchonly/models.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from sqlite3 import Row
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Wallets(NamedTuple):
|
||||
id: str
|
||||
user: str
|
||||
masterpub: str
|
||||
title: str
|
||||
address_no: int
|
||||
balance: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Wallets":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class Mempool(NamedTuple):
|
||||
user: str
|
||||
endpoint: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Mempool":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class Addresses(NamedTuple):
|
||||
id: str
|
||||
address: str
|
||||
wallet: str
|
||||
amount: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Addresses":
|
||||
return cls(**dict(row))
|
244
lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
Normal file
244
lnbits/extensions/watchonly/templates/watchonly/_api_docs.html
Normal file
|
@ -0,0 +1,244 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
Watch Only extension uses mempool.space<br />
|
||||
For use with "account Extended Public Key"
|
||||
<a href="https://iancoleman.io/bip39/">https://iancoleman.io/bip39/</a>
|
||||
<small>
|
||||
<br />Created by,
|
||||
<a target="_blank" href="https://github.com/arcbtc">Ben Arc</a> (using,
|
||||
<a target="_blank" href="https://github.com/diybitcoinhardware/embit"
|
||||
>Embit</a
|
||||
></small
|
||||
>)
|
||||
</p>
|
||||
</q-card-section>
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List wallets">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /watchonly/api/v1/wallet</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<wallets_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get wallet details"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/watchonly/api/v1/wallet/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>[<wallet_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/wallet/<wallet_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create wallet">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">POST</span> /watchonly/api/v1/wallet</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>[<wallet_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title":
|
||||
<string>, "masterpub": <string>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete wallet"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/watchonly/api/v1/wallet/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="List addresses">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/watchonly/api/v1/addresses/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<address_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get fresh address"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/watchonly/api/v1/address/<wallet_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<address_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/address/<wallet_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get mempool.space details"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /watchonly/api/v1/mempool</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<mempool_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Update mempool.space"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">POST</span>
|
||||
/watchonly/api/v1/mempool</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>[<mempool_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint":
|
||||
<string>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
</q-card>
|
474
lnbits/extensions/watchonly/templates/watchonly/index.html
Normal file
474
lnbits/extensions/watchonly/templates/watchonly/index.html
Normal file
|
@ -0,0 +1,474 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New wallet
|
||||
</q-btn>
|
||||
<q-btn unelevated color="deep-purple" icon="edit">
|
||||
<div class="cursor-pointer">
|
||||
<q-tooltip> Point to another Mempool </q-tooltip>
|
||||
{{ this.mempool.endpoint }}
|
||||
<q-popup-edit v-model="mempool.endpoint">
|
||||
<q-input color="accent" v-model="mempool.endpoint"> </q-input>
|
||||
<center>
|
||||
<q-btn flat dense @click="updateMempool()" v-close-popup
|
||||
>set</q-btn
|
||||
>
|
||||
<q-btn flat dense v-close-popup>cancel</q-btn>
|
||||
</center>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-input
|
||||
borderless
|
||||
dense
|
||||
debounce="300"
|
||||
v-model="filter"
|
||||
placeholder="Search"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="walletLinks"
|
||||
row-key="id"
|
||||
:columns="WalletsTable.columns"
|
||||
:pagination.sync="WalletsTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="dns"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
>
|
||||
<q-tooltip> Adresses </q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteWalletLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endraw %}
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Watch Only Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "watchonly/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.title"
|
||||
type="text"
|
||||
label="Title"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="formDialog.data.masterpub"
|
||||
height="50px"
|
||||
autogrow
|
||||
label="Account Extended Public Key; xpub, ypub, zpub; Bitcoin Descriptor"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="
|
||||
formDialog.data.masterpub == null ||
|
||||
formDialog.data.title == null"
|
||||
type="submit"
|
||||
>Create Watch-only Wallet</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="Addresses.show" position="top">
|
||||
<q-card v-if="Addresses.data" class="q-pa-lg lnbits__dialog-card">
|
||||
{% raw %}
|
||||
<h5 class="text-subtitle1 q-my-none">Addresses</h5>
|
||||
<q-separator></q-separator><br />
|
||||
<p>
|
||||
<strong>Current:</strong>
|
||||
{{ currentaddress }}
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="ms"
|
||||
icon="visibility"
|
||||
type="a"
|
||||
:href="mempool.endpoint + '/address/' + currentaddress"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</p>
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="currentaddress"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<p style="word-break: break-all">
|
||||
<q-scroll-area style="height: 200px; max-width: 100%">
|
||||
<q-list bordered v-for="data in Addresses.data.slice().reverse()">
|
||||
<q-item>
|
||||
<q-item-section>{{ data.address }}</q-item-section>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="ms"
|
||||
icon="visibility"
|
||||
type="a"
|
||||
:href="mempool.endpoint + '/address/' + data.address"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="getFreshAddress(current)"
|
||||
class="q-ml-sm"
|
||||
>Get fresh address</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endraw %}
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style></style>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
Vue.filter('reverse', function (value) {
|
||||
// slice to make a copy of array, then reverse the copy
|
||||
return value.slice().reverse()
|
||||
})
|
||||
var locationPath = [
|
||||
window.location.protocol,
|
||||
'//',
|
||||
window.location.hostname,
|
||||
window.location.pathname
|
||||
].join('')
|
||||
|
||||
var mapWalletLink = function (obj) {
|
||||
obj._data = _.clone(obj)
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
return obj
|
||||
}
|
||||
var mapAddresses = function (obj) {
|
||||
obj._data = _.clone(obj)
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
filter: '',
|
||||
balance: null,
|
||||
checker: null,
|
||||
walletLinks: [],
|
||||
AddressesLinks: [],
|
||||
currentaddress: '',
|
||||
Addresses: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
mempool: {
|
||||
endpoint: ''
|
||||
},
|
||||
WalletsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'left',
|
||||
label: 'Title',
|
||||
field: 'title'
|
||||
},
|
||||
{
|
||||
name: 'masterpub',
|
||||
align: 'left',
|
||||
label: 'MasterPub',
|
||||
field: 'masterpub'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getAddressDetails: function (address) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/mempool/' + address,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
return reponse.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getAddresses: function (walletID) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/addresses/' + walletID,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.Addresses.data = response.data
|
||||
self.currentaddress =
|
||||
self.Addresses.data[self.Addresses.data.length - 1].address
|
||||
self.AddressesLinks = response.data.map(function (obj) {
|
||||
return mapAddresses(obj)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getFreshAddress: function (walletID) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/address/' + walletID,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.Addresses.data = response.data
|
||||
self.currentaddress =
|
||||
self.Addresses.data[self.Addresses.data.length - 1].address
|
||||
})
|
||||
},
|
||||
getMempool: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/mempool',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.mempool.endpoint = response.data.endpoint
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
|
||||
updateMempool: function () {
|
||||
var self = this
|
||||
var wallet = this.g.user.wallets[0]
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/watchonly/api/v1/mempool',
|
||||
wallet.adminkey,
|
||||
self.mempool
|
||||
)
|
||||
.then(function (response) {
|
||||
self.mempool.endpoint = response.data.endpoint
|
||||
self.walletLinks.push(mapwalletLink(response.data))
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getWalletLinks: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/wallet',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.walletLinks = response.data.map(function (obj) {
|
||||
self.getAddresses(obj.id)
|
||||
return mapWalletLink(obj)
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
openQrCodeDialog: function (linkId) {
|
||||
var self = this
|
||||
var getAddresses = this.getAddresses
|
||||
this.getAddresses(linkId)
|
||||
self.current = linkId
|
||||
self.Addresses.show = true
|
||||
},
|
||||
sendFormData: function () {
|
||||
var wallet = this.g.user.wallets[0]
|
||||
var data = _.omit(this.formDialog.data, 'wallet')
|
||||
this.createWalletLink(wallet, data)
|
||||
},
|
||||
createWalletLink: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('POST', '/watchonly/api/v1/wallet', wallet.adminkey, data)
|
||||
.then(function (response) {
|
||||
self.walletLinks.push(mapWalletLink(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWalletLink: function (linkId) {
|
||||
var self = this
|
||||
var link = _.findWhere(this.walletLinks, {id: linkId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/watchonly/api/v1/wallet/' + linkId,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.walletLinks = _.reject(self.walletLinks, function (obj) {
|
||||
return obj.id === linkId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
var getMempool = this.getMempool
|
||||
getMempool()
|
||||
var getWalletLinks = this.getWalletLinks
|
||||
getWalletLinks()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
22
lnbits/extensions/watchonly/views.py
Normal file
22
lnbits/extensions/watchonly/views.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from quart import g, abort, render_template
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from . import watchonly_ext
|
||||
|
||||
|
||||
@watchonly_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("watchonly/index.html", user=g.user)
|
||||
|
||||
|
||||
@watchonly_ext.route("/<charge_id>")
|
||||
async def display(charge_id):
|
||||
link = get_payment(charge_id) or abort(
|
||||
HTTPStatus.NOT_FOUND, "Charge link does not exist."
|
||||
)
|
||||
|
||||
return await render_template("watchonly/display.html", link=link)
|
138
lnbits/extensions/watchonly/views_api.py
Normal file
138
lnbits/extensions/watchonly/views_api.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
import hashlib
|
||||
from quart import g, jsonify, url_for, request
|
||||
from http import HTTPStatus
|
||||
import httpx
|
||||
import json
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from lnbits.extensions.watchonly import watchonly_ext
|
||||
from .crud import (
|
||||
create_watch_wallet,
|
||||
get_watch_wallet,
|
||||
get_watch_wallets,
|
||||
update_watch_wallet,
|
||||
delete_watch_wallet,
|
||||
get_fresh_address,
|
||||
get_addresses,
|
||||
create_mempool,
|
||||
update_mempool,
|
||||
get_mempool,
|
||||
)
|
||||
|
||||
###################WALLETS#############################
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/wallet", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_wallets_retrieve():
|
||||
|
||||
try:
|
||||
return (
|
||||
jsonify(
|
||||
[wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_wallet_retrieve(wallet_id):
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
return jsonify(wallet._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/wallet", methods=["POST"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"masterpub": {"type": "string", "empty": False, "required": True},
|
||||
"title": {"type": "string", "empty": False, "required": True},
|
||||
}
|
||||
)
|
||||
async def api_wallet_create_or_update(wallet_id=None):
|
||||
try:
|
||||
wallet = await create_watch_wallet(
|
||||
user=g.wallet.user, masterpub=g.data["masterpub"], title=g.data["title"]
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
|
||||
mempool = await get_mempool(g.wallet.user)
|
||||
if not mempool:
|
||||
create_mempool(user=g.wallet.user)
|
||||
return jsonify(wallet._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_wallet_delete(wallet_id):
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
await delete_watch_wallet(wallet_id)
|
||||
|
||||
return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#############################ADDRESSES##########################
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/address/<wallet_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_fresh_address(wallet_id):
|
||||
await get_fresh_address(wallet_id)
|
||||
|
||||
addresses = await get_addresses(wallet_id)
|
||||
|
||||
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/addresses/<wallet_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_get_addresses(wallet_id):
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
if not wallet:
|
||||
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
addresses = await get_addresses(wallet_id)
|
||||
|
||||
if not addresses:
|
||||
await get_fresh_address(wallet_id)
|
||||
addresses = await get_addresses(wallet_id)
|
||||
|
||||
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK
|
||||
|
||||
|
||||
#############################MEMPOOL##########################
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/mempool", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"endpoint": {"type": "string", "empty": False, "required": True},
|
||||
}
|
||||
)
|
||||
async def api_update_mempool():
|
||||
mempool = await update_mempool(user=g.wallet.user, **g.data)
|
||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@watchonly_ext.route("/api/v1/mempool", methods=["GET"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_get_mempool():
|
||||
mempool = await get_mempool(g.wallet.user)
|
||||
if not mempool:
|
||||
mempool = await create_mempool(user=g.wallet.user)
|
||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
|
@ -48,3 +48,4 @@ trio==0.16.0
|
|||
typing-extensions==3.7.4.3
|
||||
werkzeug==1.0.1
|
||||
wsproto==1.0.0
|
||||
embit==0.2.1
|
Loading…
Reference in New Issue
Block a user