Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7210cf406 | ||
|
|
8e8e4d5c4e | ||
|
|
c5db665b6d | ||
|
|
602c439d43 | ||
|
|
9303e68e8c | ||
|
|
9edc4786e1 | ||
|
|
93dc10fe94 | ||
|
|
6c8448d7a8 | ||
|
|
99e4f33142 | ||
|
|
f4f43ad361 | ||
|
|
f14ea6c577 | ||
|
|
07428ecf94 | ||
|
|
867e3d06f6 | ||
|
|
4b4b6d0bcd | ||
|
|
07b1521dad | ||
|
|
7a2ddd9826 | ||
|
|
0eb4b477b7 | ||
|
|
06a0ba58ce | ||
|
|
5399b36027 | ||
|
|
385fb4f9bc | ||
|
|
8db76b8864 | ||
|
|
9e3ab0ef26 | ||
|
|
04c9b67997 | ||
|
|
116f982aab | ||
|
|
63cc89e2b6 | ||
|
|
183e6e5661 | ||
|
|
6b3fd80e46 | ||
|
|
5999a773be | ||
|
|
7e0fadad3b | ||
|
|
b2d6243697 | ||
|
|
6b5a77fb3b | ||
|
|
cc8fb68b02 | ||
|
|
658da6b28e | ||
|
|
7d734ecb74 | ||
|
|
9177dd195b | ||
|
|
15faab4f38 | ||
|
|
2cce687865 | ||
|
|
313bd3f647 | ||
|
|
13a93836d9 | ||
|
|
4f76d0483e | ||
|
|
719d86aa9c | ||
|
|
bbad4a91ae | ||
|
|
fcebb7e28c | ||
|
|
ce5aa4c8a7 | ||
|
|
75bae67446 | ||
|
|
8c184356ef | ||
|
|
efc0547271 | ||
|
|
7a393b11fd | ||
|
|
fae3eca3c7 | ||
|
|
3398070dd5 | ||
|
|
7d98c6b080 | ||
|
|
5f4e889d3a | ||
|
|
fe774ae1a1 | ||
|
|
21e02cb20e | ||
|
|
a3d7b463ae | ||
|
|
e1e2112461 | ||
|
|
92fc9a130f | ||
|
|
e15f3cd207 | ||
|
|
041b358310 | ||
|
|
cbd3de88c4 | ||
|
|
156ab10ad6 | ||
|
|
b77c103020 | ||
|
|
1f34357ab7 | ||
|
|
d9eda48298 | ||
|
|
1be2d1a680 | ||
|
|
b55ae9daaf | ||
|
|
1f98aa2cdb | ||
|
|
55ec3f1812 | ||
|
|
7880044483 | ||
|
|
28b3f79d9b | ||
|
|
4c704305ac | ||
|
|
c47fd2a4d1 | ||
|
|
1255df43e6 | ||
|
|
6ccc10d327 | ||
|
|
8bcf5e4f70 | ||
|
|
04ec643c93 | ||
|
|
f93efbfb3a | ||
|
|
4429c0cdb6 | ||
|
|
2bdb89b4b9 | ||
|
|
75754598e1 | ||
|
|
b25a3c2bb4 | ||
|
|
e7c4de6506 | ||
|
|
c102f10fab | ||
|
|
7c28b58aaa | ||
|
|
2b27f13fa8 | ||
|
|
130f4fd0bc | ||
|
|
4894b70bad | ||
|
|
05769240d3 | ||
|
|
7bd127d22c | ||
|
|
6dc3df148d | ||
|
|
e286546b63 | ||
|
|
eae1be1db8 | ||
|
|
25ab676a3f | ||
|
|
2fd6648a9a | ||
|
|
8eda653e3f | ||
|
|
7e327b2c6a | ||
|
|
40c055e373 | ||
|
|
1d9808b269 | ||
|
|
c65ab2af87 | ||
|
|
15f093c136 | ||
|
|
5311b0a393 | ||
|
|
e72396fe31 | ||
|
|
d6d0ebb20e | ||
|
|
563dd5b499 | ||
|
|
9be894da74 | ||
|
|
87b66c554a | ||
|
|
314b533e39 | ||
|
|
af8f3382c1 | ||
|
|
67bb86c1ce | ||
|
|
f0f9e41f99 | ||
|
|
2fbaf57b5e | ||
|
|
87afba2abb | ||
|
|
c375bf1b15 | ||
|
|
dbcc920d0a | ||
|
|
2c61e219b8 | ||
|
|
6cb9b7da1f | ||
|
|
83334d67c8 | ||
|
|
ef9adc077b | ||
|
|
ffad5d79c4 | ||
|
|
09c35eaca0 | ||
|
|
52adc62bc4 | ||
|
|
0efa173084 | ||
|
|
724f8c9f63 | ||
|
|
c6981089e5 | ||
|
|
13480027dd | ||
|
|
93f1006d3a | ||
|
|
5490367179 | ||
|
|
a185197e34 | ||
|
|
aa1bddcdd5 | ||
|
|
6d24ba55aa | ||
|
|
5b2937672c | ||
|
|
957d9ce419 | ||
|
|
0e21b77b74 | ||
|
|
24e28911ab | ||
|
|
1e252d0485 | ||
|
|
e98550a125 | ||
|
|
bc55474859 | ||
|
|
d985bdaadb | ||
|
|
8e0d1cc100 | ||
|
|
cb3fd56647 | ||
|
|
9f383bfee6 | ||
|
|
3b2e28dd60 | ||
|
|
132192bc94 | ||
|
|
281c3df826 | ||
|
|
08591f34c2 | ||
|
|
f0e8ae0f5c | ||
|
|
168cb726b1 | ||
|
|
91ac245307 | ||
|
|
d098e2f710 | ||
|
|
a0f65f4cda | ||
|
|
606ee215b4 | ||
|
|
7716f819f6 | ||
|
|
8325d880cb | ||
|
|
b090470fc1 | ||
|
|
c7297d2e77 | ||
|
|
b6e111b21c | ||
|
|
7747d7b741 | ||
|
|
f3a5a8e002 | ||
|
|
157a6485b4 | ||
|
|
8867b27b09 | ||
|
|
68b607ecbc | ||
|
|
f411fd13dc | ||
|
|
baee90da67 | ||
|
|
661b713993 | ||
|
|
5a5f253fa5 | ||
|
|
77a5d7fe50 | ||
|
|
07dd4fc685 | ||
|
|
3761f7922c | ||
|
|
327b9d7f63 | ||
|
|
cacffc67ee | ||
|
|
b7fdf99a8d | ||
|
|
cd6cfff9cf | ||
|
|
7b635a31e5 | ||
|
|
b4c0cdbc7c | ||
|
|
fd765e2060 | ||
|
|
5f86627eae | ||
|
|
3af3838995 | ||
|
|
a762529fef | ||
|
|
245569d0b9 | ||
|
|
fcaeb0ac7a | ||
|
|
d2aedde21b | ||
|
|
5adc419c74 | ||
|
|
5d79327906 | ||
|
|
71e0b396d2 | ||
|
|
89d673448a | ||
|
|
aed3f3b569 | ||
|
|
d9a2a7bb95 | ||
|
|
15ede13104 | ||
|
|
62047ea758 | ||
|
|
e5e589fc40 | ||
|
|
46406792fe | ||
|
|
dbf71fed53 | ||
|
|
850087a8ec | ||
|
|
d9b045c526 | ||
|
|
e1ca6ef82a | ||
|
|
17c40d539e | ||
|
|
625fa6503c | ||
|
|
73634e5161 | ||
|
|
1fe35070f4 | ||
|
|
ad268516a9 | ||
|
|
ca94909aab | ||
|
|
b3efb4d378 | ||
|
|
5213508dc1 | ||
|
|
af9331eede | ||
|
|
9b8fe42102 | ||
|
|
a0e4a59454 | ||
|
|
eefaf3c50c | ||
|
|
89cabda123 | ||
|
|
6122a03f32 | ||
|
|
ad8b70a098 | ||
|
|
6449276003 | ||
|
|
4da651b74a | ||
|
|
b5f3a46feb | ||
|
|
1057b4693f | ||
|
|
5711b4b804 | ||
|
|
704e5a1b73 | ||
|
|
e6ca8a33c6 | ||
|
|
9c257aa23d | ||
|
|
ef752cbeca | ||
|
|
a509eb8fdb | ||
|
|
49cc8104fc | ||
|
|
f1f6af0e35 | ||
|
|
5f1cfc0e37 | ||
|
|
730ab59578 | ||
|
|
c8bdba8e53 | ||
|
|
c6c67c52db | ||
|
|
da31e3caaa | ||
|
|
d0bf47163b | ||
|
|
37ad515427 | ||
|
|
8c77f75cf1 | ||
|
|
21505471d5 | ||
|
|
baa9a35773 | ||
|
|
7f114ddcc0 | ||
|
|
92aad20dd7 | ||
|
|
0f4ae5da86 | ||
|
|
7a796c6510 | ||
|
|
0910687328 | ||
|
|
33e2fc2ea8 | ||
|
|
0c6e8394c8 | ||
|
|
d55e2a0e1f | ||
|
|
148ba9d275 | ||
|
|
d2ca774f6f | ||
|
|
233398b512 | ||
|
|
152c1dbb74 | ||
|
|
44985cb0d1 | ||
|
|
b1a7692ce4 | ||
|
|
31bff37b1e | ||
|
|
3f7a1798d5 | ||
|
|
98d4dbab8b | ||
|
|
7c7a04da9d | ||
|
|
3ccefb70fa | ||
|
|
5346ac47b1 | ||
|
|
9f4ed53888 | ||
|
|
faac56c14c | ||
|
|
3e0d0d9896 | ||
|
|
b9a004a5e4 | ||
|
|
da3cb548bd | ||
|
|
4144359617 | ||
|
|
07268b87d2 | ||
|
|
c356874bcb | ||
|
|
f9b5c7db7a | ||
|
|
20f9ddb57c | ||
|
|
ee57ba4c1a | ||
|
|
ff7f5c1ca5 | ||
|
|
1463d75ee2 | ||
|
|
f1fc4710ee | ||
|
|
00eaec8290 | ||
|
|
28c4235c94 | ||
|
|
b7d178c08e | ||
|
|
9d0ec97d39 | ||
|
|
b6f533be24 | ||
|
|
5e36bb4fcd | ||
|
|
f9b0604711 | ||
|
|
39c33699af | ||
|
|
c89721223f | ||
|
|
1e6e97c12d | ||
|
|
783efb19db | ||
|
|
9ffc63b5dc | ||
|
|
0ec8139e5c | ||
|
|
a8c3181852 | ||
|
|
e038ceb9be | ||
|
|
b4eccb9e5d | ||
|
|
691b7ad055 | ||
|
|
c84d1b66c6 | ||
|
|
cf7726ddfd | ||
|
|
bd50a3c546 | ||
|
|
c9270bbf7f | ||
|
|
6474aeb982 | ||
|
|
37ba437ad1 | ||
|
|
35af181e24 | ||
|
|
f8c58aef0e | ||
|
|
8755984bd8 | ||
|
|
0d5661cda7 | ||
|
|
c2a3fbc6c0 | ||
|
|
6bac34fc6b | ||
|
|
b54eedee84 | ||
|
|
bd07f7a5ef | ||
|
|
a40306f5cd | ||
|
|
2fecec2623 | ||
|
|
4ad89396cc | ||
|
|
82307c4839 | ||
|
|
8506199c2f | ||
|
|
d142d76148 | ||
|
|
98e9a61b98 | ||
|
|
877af0c21e | ||
|
|
dd27b190f3 | ||
|
|
2856803ca7 | ||
|
|
ca8264b1f5 | ||
|
|
39ca9da870 | ||
|
|
2b603bdc48 | ||
|
|
26780df065 | ||
|
|
248fcc06ab | ||
|
|
f794373d30 | ||
|
|
87f25ea715 | ||
|
|
b9de754598 | ||
|
|
785fb7af8e | ||
|
|
55c5ab3a6d | ||
|
|
bd07a319ab | ||
|
|
6b732a2a6a | ||
|
|
25c8dd18e0 | ||
|
|
2b1b5cadaa | ||
|
|
6b41820422 | ||
|
|
7116353431 | ||
|
|
de6827af58 | ||
|
|
40c065708a | ||
|
|
4cf9fae3e3 | ||
|
|
bf06def9b7 | ||
|
|
182894fd93 | ||
|
|
d0cf374cda | ||
|
|
1e0dab32c3 | ||
|
|
34c9e218bb | ||
|
|
9cc6a96433 | ||
|
|
e5ff928c3c | ||
|
|
f31ffba06c | ||
|
|
1163e44265 | ||
|
|
266b16834b | ||
|
|
ad606b2517 | ||
|
|
1ec28079d9 | ||
|
|
aeaddc1dc6 | ||
|
|
a9374d509c | ||
|
|
3e72788fc8 | ||
|
|
f48d24aca8 | ||
|
|
015262c9b3 | ||
|
|
e5e481f836 | ||
|
|
6c32ebf7e6 | ||
|
|
7c88702754 | ||
|
|
210b8a7c18 | ||
|
|
4f9a5090c2 | ||
|
|
c05122e5fb | ||
|
|
c054e47913 | ||
|
|
92d3269a85 | ||
|
|
183b84c167 | ||
|
|
60f50a71a2 | ||
|
|
c3252ce4dc | ||
|
|
c312d70e63 | ||
|
|
36fc911b88 | ||
|
|
672a5b3a4d | ||
|
|
c0b33560bb | ||
|
|
15b6b1d512 | ||
|
|
67c92a79cf | ||
|
|
0d4751d6e0 | ||
|
|
213bde61a8 | ||
|
|
59553a6e1e | ||
|
|
918add5e6e | ||
|
|
2375f7bcf8 | ||
|
|
5021570f68 | ||
|
|
0d91d5b5be | ||
|
|
49f02a5b77 | ||
|
|
95b9dabb60 | ||
|
|
bc0087ec36 | ||
|
|
8272147012 | ||
|
|
244de2ae86 | ||
|
|
ef371e303c | ||
|
|
5ba06d42d0 | ||
|
|
15984fa49b | ||
|
|
67dc096909 | ||
|
|
2daddf8b74 | ||
|
|
ae6cf47244 | ||
|
|
1488f580ff | ||
|
|
609c949394 | ||
|
|
c24d2f24cf | ||
|
|
0ba81ec719 | ||
|
|
5e03707be5 | ||
|
|
50f1111356 | ||
|
|
112a0f9698 | ||
|
|
75016d1a27 | ||
|
|
5b9f244a9f | ||
|
|
0d2ff2e663 | ||
|
|
bdfefc2506 | ||
|
|
2a9f471783 | ||
|
|
793c7c45ed | ||
|
|
ff535ae065 | ||
|
|
01fdc1defb | ||
|
|
dce2bfb440 | ||
|
|
a36ab2d408 | ||
|
|
98f732fb6f | ||
|
|
65864cb216 | ||
|
|
114e7b8c3c | ||
|
|
fbfab880f3 | ||
|
|
133afff8ee | ||
|
|
851c952c6d | ||
|
|
8b29148085 | ||
|
|
ad48092549 | ||
|
|
ee449ca612 | ||
|
|
73f45ae859 | ||
|
|
094a9fc1a1 | ||
|
|
5e9919c5c5 | ||
|
|
f91c933919 | ||
|
|
6e9f451419 | ||
|
|
256a8098c6 | ||
|
|
1a02095758 | ||
|
|
ec5b7493b3 | ||
|
|
cdc1a6dc8f | ||
|
|
7d573ef859 | ||
|
|
4bd2a4fba9 | ||
|
|
209c251fb6 | ||
|
|
69a7ef74fd | ||
|
|
35e2c4b0a7 | ||
|
|
2c6dce808e | ||
|
|
fa89313e6f | ||
|
|
76ecf113c3 | ||
|
|
e0749e186e | ||
|
|
0529ee2835 | ||
|
|
09b91f9f79 | ||
|
|
90ab642dcd | ||
|
|
88cf1ac853 | ||
|
|
9aa2194d94 | ||
|
|
ba0f7a01ef | ||
|
|
5765ea0276 | ||
|
|
88b01e6079 | ||
|
|
b0d0901969 | ||
|
|
6563610b6c | ||
|
|
c4aa0b598c | ||
|
|
aab76e691d | ||
|
|
3ed30be93e | ||
|
|
37f99e7f96 | ||
|
|
a16078a6ba | ||
|
|
c4c03d96a3 | ||
|
|
ab3248da15 | ||
|
|
b4b37cd733 | ||
|
|
26eb7a449c | ||
|
|
695e9b6471 | ||
|
|
5c9511ccfe | ||
|
|
c7b7832a88 | ||
|
|
ff24847980 | ||
|
|
659ec31edd | ||
|
|
edb7d66efc | ||
|
|
efcc5e2148 | ||
|
|
55889abd88 | ||
|
|
063aa4fd06 | ||
|
|
7c0a955b30 | ||
|
|
b1a09af82c | ||
|
|
6d2f39d390 | ||
|
|
18496f0dd9 | ||
|
|
acf6c732ad | ||
|
|
b39bd257e0 | ||
|
|
9aeecd4dc0 | ||
|
|
e750165cc3 | ||
|
|
77906bc817 | ||
|
|
63e728710d | ||
|
|
27fd510142 | ||
|
|
e6de66e1b1 | ||
|
|
4071925f65 | ||
|
|
3c4d186dba | ||
|
|
56aebb9d8f | ||
|
|
beee24bd92 | ||
|
|
375b95c004 | ||
|
|
5345ccaf4e | ||
|
|
efc5233399 | ||
|
|
e4d09c6d12 | ||
|
|
7d0545dae1 | ||
|
|
00fccf513e | ||
|
|
34b8490a2d | ||
|
|
cbbba5c4c7 | ||
|
|
4a0ef7fa1a | ||
|
|
cd8804daca | ||
|
|
39e4fa724a | ||
|
|
3b350858c7 | ||
|
|
7bea591879 | ||
|
|
f74fcea35b | ||
|
|
ceb8203353 | ||
|
|
7b29223e95 | ||
|
|
bd19d78834 | ||
|
|
8458b4d84b | ||
|
|
f92ae8bae1 | ||
|
|
3529f9152f | ||
|
|
6a9089fd98 | ||
|
|
5818c3c2ba | ||
|
|
a38de94e49 | ||
|
|
b4801ce0ab | ||
|
|
51bf344d65 | ||
|
|
32cbf16d91 | ||
|
|
c4d0540e76 | ||
|
|
f8b3644029 | ||
|
|
c2fb45d640 | ||
|
|
e339bb6181 | ||
|
|
2dee26b728 | ||
|
|
d774c7a742 | ||
|
|
6c2b312c92 | ||
|
|
56c8783d9a | ||
|
|
ffecd03c4b | ||
|
|
94d5f37723 | ||
|
|
d89bd7409d | ||
|
|
b48489ef32 | ||
|
|
e686ecda51 | ||
|
|
23b4c2cc16 | ||
|
|
1bb29cf0b1 | ||
|
|
5714848434 | ||
|
|
f87e5d4372 | ||
|
|
a0502f1676 | ||
|
|
f6c8a308dc | ||
|
|
6df3933c1b | ||
|
|
ffef085fa7 | ||
|
|
913991c72f | ||
|
|
6f611461f2 | ||
|
|
681385e2a2 | ||
|
|
883d52c303 | ||
|
|
1058461882 | ||
|
|
3a7a88857a | ||
|
|
6834b5e00f | ||
|
|
10093bb465 | ||
|
|
1323a2005b | ||
|
|
bafb4ddf75 | ||
|
|
aa050eaf49 | ||
|
|
0c76efa2b9 | ||
|
|
681730a4af | ||
|
|
63adcb6780 | ||
|
|
35f7821183 | ||
|
|
b185d9585c | ||
|
|
30a8d88ada | ||
|
|
f7e984198b | ||
|
|
eb09e95e88 | ||
|
|
a883e2c7d8 | ||
|
|
e06abb52ce | ||
|
|
9f64b5349d | ||
|
|
a6bbddce41 | ||
|
|
adb9d24673 | ||
|
|
dc85f26964 | ||
|
|
14153b4a8f | ||
|
|
b8a5f3942c | ||
|
|
475ae5736e | ||
|
|
d7f0f4da0c | ||
|
|
f7b3444be4 | ||
|
|
28e32a54a4 | ||
|
|
1833adc0a1 | ||
|
|
b759ec7468 | ||
|
|
2661d12ff1 | ||
|
|
ed419f27ac | ||
|
|
c3efd48108 | ||
|
|
4a28c22d79 | ||
|
|
0d0eb36de4 | ||
|
|
2aa73bfe75 | ||
|
|
ebd080192c | ||
|
|
41abe63f18 | ||
|
|
f61471b0f2 | ||
|
|
9bd037b6e7 | ||
|
|
a4ca88b6a4 | ||
|
|
f2b9aafc51 | ||
|
|
ec77a00f49 | ||
|
|
991e0db50b | ||
|
|
fe9b62e8a8 | ||
|
|
c5964436b5 | ||
|
|
4ce14e2312 | ||
|
|
7021add193 | ||
|
|
4be01a405c | ||
|
|
ebe9ff700f | ||
|
|
a3619d40c6 | ||
|
|
048aff3db4 | ||
|
|
89d85a0d7e | ||
|
|
5aa1f9b0f8 | ||
|
|
5dc1705fa6 | ||
|
|
0fc657c55c | ||
|
|
8641b34a3d | ||
|
|
30cf6913af | ||
|
|
10682cf736 | ||
|
|
ee725846c5 | ||
|
|
69b31b1480 | ||
|
|
73a57ecf97 | ||
|
|
ab0f52d811 | ||
|
|
8c59f3f4ae | ||
|
|
1bfb09ddab | ||
|
|
2af5ac9678 | ||
|
|
4511891297 | ||
|
|
eeca7de10d | ||
|
|
b06c16ed57 | ||
|
|
2f2a3b1092 | ||
|
|
9fe63b49d6 | ||
|
|
6754ffb9dc | ||
|
|
4afe6dbd6d | ||
|
|
0cd94e654e | ||
|
|
d1692a18dd | ||
|
|
0305d740ee | ||
|
|
05777ca953 | ||
|
|
1e083856ed | ||
|
|
c08623277e | ||
|
|
60de308c8f | ||
|
|
2aebe72df8 | ||
|
|
2bd7600bcf | ||
|
|
e318f7679e | ||
|
|
0912d7d1c8 | ||
|
|
bfa23568e3 | ||
|
|
b76d8b5458 | ||
|
|
1cef614349 | ||
|
|
16fbfd1c6f | ||
|
|
15ba3c33a3 | ||
|
|
b33a1903e8 | ||
|
|
2d41a1bed3 | ||
|
|
4d490506f3 | ||
|
|
ed3a54e57a | ||
|
|
61e8d0059b | ||
|
|
4a640dc123 | ||
|
|
1f4160fda5 | ||
|
|
dc91fbea9b | ||
|
|
7eb021f43c | ||
|
|
69350849b1 | ||
|
|
828261d696 | ||
|
|
90dd3cc73a | ||
|
|
10c60291d2 | ||
|
|
9d2e52f694 | ||
|
|
503a599341 | ||
|
|
a8809ed5a4 | ||
|
|
e134c5c7b9 | ||
|
|
0efb52664e | ||
|
|
53b00c6d41 | ||
|
|
9a1d707f54 | ||
|
|
736699af94 | ||
|
|
432b3a0fe0 | ||
|
|
34a959f0bc | ||
|
|
c1d26bb274 | ||
|
|
b24a65cf2a | ||
|
|
960c58db87 | ||
|
|
dd9b217fdf | ||
|
|
7af9da4a58 | ||
|
|
c23eae9e08 | ||
|
|
66b2ca2ac4 | ||
|
|
25e5423511 | ||
|
|
4c7567a552 | ||
|
|
73e716085b | ||
|
|
cc33a49b12 | ||
|
|
eeebaffbed | ||
|
|
8633891485 | ||
|
|
cb5cc1e5e8 | ||
|
|
f845bfe651 | ||
|
|
b6bdf50ed7 | ||
|
|
56a4b702f3 | ||
|
|
273ab9781f | ||
|
|
8a759c8fc1 | ||
|
|
c7ebe09463 | ||
|
|
5d4d22dcdd | ||
|
|
b164317121 | ||
|
|
f415a92914 | ||
|
|
16c78f1901 | ||
|
|
6a08d20fe8 | ||
|
|
5abfbdd07a | ||
|
|
bba7614012 | ||
|
|
2ee7180aa0 | ||
|
|
982732cbd4 | ||
|
|
dfdb34852c | ||
|
|
8fa5c81088 | ||
|
|
91748fc891 | ||
|
|
0574837950 | ||
|
|
7e27b2c6f0 | ||
|
|
6f3d0edfbd | ||
|
|
3900d2871d | ||
|
|
0c5b909c7a | ||
|
|
78437eaf94 | ||
|
|
8d1542d982 | ||
|
|
146a74761a | ||
|
|
31664a4ba0 | ||
|
|
f985a1eb08 | ||
|
|
4e5b001901 | ||
|
|
3a2cf12052 | ||
|
|
f17ff3d12b | ||
|
|
32c042741d | ||
|
|
d234ab12d8 | ||
|
|
37187bfc2c | ||
|
|
368da935db | ||
|
|
81aef06f75 | ||
|
|
27241e01f3 | ||
|
|
3ff85363f2 | ||
|
|
b81b30c896 | ||
|
|
25d59e4142 | ||
|
|
5f8ccee5b8 | ||
|
|
740180d332 | ||
|
|
b66a8b3de9 | ||
|
|
b3351f2b17 | ||
|
|
36dbdaa159 | ||
|
|
524a4c9213 | ||
|
|
200b9b127c | ||
|
|
646c341840 | ||
|
|
826f8ad2ac | ||
|
|
1b9bb32ca0 | ||
|
|
61c71660b8 | ||
|
|
80ac158ca0 | ||
|
|
291c69e470 | ||
|
|
8ff4962e86 | ||
|
|
57e9d2271d | ||
|
|
c18816bf1b | ||
|
|
8f290a647d | ||
|
|
d1cc317044 | ||
|
|
5a1d660c7b | ||
|
|
7ee78248b7 | ||
|
|
5104cbb285 | ||
|
|
fa71219ce7 | ||
|
|
f5f43e7668 | ||
|
|
4b63d0924e | ||
|
|
dc22088dcd | ||
|
|
aa63879ff5 | ||
|
|
0547abe54a | ||
|
|
f5ccf5c157 | ||
|
|
e3d6b6befa | ||
|
|
fa8d7c665b | ||
|
|
f97f27121a | ||
|
|
af568d0f31 | ||
|
|
8c5c455f1c | ||
|
|
bbae4a01c1 | ||
|
|
edfd297bf3 | ||
|
|
09b1623bb0 | ||
|
|
fa18170ed7 | ||
|
|
3eece03715 | ||
|
|
dd972e2aaa | ||
|
|
ec9ad9f940 | ||
|
|
aced333c0b | ||
|
|
ba5f79da2d | ||
|
|
acb1b1ed91 | ||
|
|
2fa0a3c995 | ||
|
|
1dddc9e7f0 | ||
|
|
581f98b3a3 | ||
|
|
89a75ab641 | ||
|
|
f228198c20 | ||
|
|
2940cf97c5 | ||
|
|
ae4eda04ba | ||
|
|
13f2dd732f | ||
|
|
80ec9e1307 | ||
|
|
3b503eaa8a | ||
|
|
ecc62b0011 | ||
|
|
b83c2e9368 | ||
|
|
512c85592f | ||
|
|
76b01695a0 | ||
|
|
d7065392d1 | ||
|
|
a58deff70c | ||
|
|
e85a78854e | ||
|
|
a28e334c5f | ||
|
|
a4c000d7dc | ||
|
|
3a64cf5245 | ||
|
|
0b8da2b524 | ||
|
|
f062b3d5e5 | ||
|
|
b9e9da5da7 | ||
|
|
953a466119 | ||
|
|
7ad360d176 | ||
|
|
898f264951 | ||
|
|
75bcbdc18d | ||
|
|
3ac6bc3c3b | ||
|
|
04882e05a3 | ||
|
|
cf2aca3327 | ||
|
|
1d0547ea97 | ||
|
|
99c5f00c91 | ||
|
|
723d8ba98f | ||
|
|
daee2b3418 | ||
|
|
9d7e54f6b2 | ||
|
|
053ea20508 | ||
|
|
04aefc8077 | ||
|
|
21d87adc52 | ||
|
|
c637e8d31e | ||
|
|
d26e50ec9a | ||
|
|
d229b7a765 | ||
|
|
ceb43f384e | ||
|
|
22e6326bce | ||
|
|
5f4f1288d7 | ||
|
|
7a5e7fbd8c | ||
|
|
6c8d56e40c | ||
|
|
8aa1716e32 | ||
|
|
fbd22c1a22 | ||
|
|
e8a6870d7a | ||
|
|
937936ff33 | ||
|
|
ab3eb967b4 | ||
|
|
6341e1edaf | ||
|
|
69d518bac0 | ||
|
|
9db2429a45 | ||
|
|
dd90dec768 | ||
|
|
eb37a064ad | ||
|
|
28df100d9a | ||
|
|
6e6b387b7d | ||
|
|
209cf7fbe0 | ||
|
|
596167f443 | ||
|
|
4732c4b296 | ||
|
|
405a2f0776 | ||
|
|
304ad3035b | ||
|
|
8cffda5a55 | ||
|
|
cbe858b385 | ||
|
|
65ecca2507 | ||
|
|
1900cf9aa4 | ||
|
|
296b1dfa9a | ||
|
|
b14e0e4cc6 | ||
|
|
fb585402dd | ||
|
|
63f246908e | ||
|
|
8ac827f5a4 | ||
|
|
74d4ddd312 | ||
|
|
40ffa7dea0 | ||
|
|
ddb8fcb986 | ||
|
|
27b9e8254c | ||
|
|
daa4b92331 | ||
|
|
0015314e11 | ||
|
|
235f8a6c19 | ||
|
|
bab399f825 | ||
|
|
646a604221 | ||
|
|
7d8fad267a | ||
|
|
ce0aff206d | ||
|
|
80e7b9639d | ||
|
|
94caa2e1ba | ||
|
|
b0a66e8cf5 | ||
|
|
ffba71c0ce | ||
|
|
b41705167f | ||
|
|
0387db3b55 | ||
|
|
38ef1b0061 | ||
|
|
19c231a2f4 | ||
|
|
026c9b5155 | ||
|
|
c834929f8b | ||
|
|
97de4eda18 | ||
|
|
a844711530 | ||
|
|
e2522b358a | ||
|
|
8f761dfd0f | ||
|
|
b14d36a0aa | ||
|
|
dbb689c5c5 | ||
|
|
2167aa398f | ||
|
|
eb8d2f312f | ||
|
|
f9133760fc | ||
|
|
7298c4664b | ||
|
|
eda7e35c61 | ||
|
|
7d1e22c7de | ||
|
|
1bee84d419 | ||
|
|
a00292544f | ||
|
|
fe14c2cd83 | ||
|
|
760f11f1ce | ||
|
|
0e1090b717 | ||
|
|
b2564154cd | ||
|
|
fb17611207 | ||
|
|
7f628948c9 | ||
|
|
cbe0861439 | ||
|
|
e9d6160f4d | ||
|
|
eacdd432b2 | ||
|
|
76e8d72d0d | ||
|
|
dbacf7e8c1 | ||
|
|
b6d99b09cf | ||
|
|
5c21e7f9ed | ||
|
|
14e9c7d9dc | ||
|
|
b3368d89f4 | ||
|
|
83b89851a5 | ||
|
|
2db5a83f4e | ||
|
|
d72cf40439 | ||
|
|
7c68a02eee | ||
|
|
93965bc5b6 | ||
|
|
ae60b4517c | ||
|
|
b15596d045 | ||
|
|
07f0dc80f8 | ||
|
|
5f64c298c9 | ||
|
|
5b056ce07e | ||
|
|
44b458ebb8 | ||
|
|
d4da96597e | ||
|
|
6a0b645316 | ||
|
|
35bb3cc94b | ||
|
|
7a6c3646fb | ||
|
|
b84161c49d | ||
|
|
63ce506d29 | ||
|
|
5114bd4a47 | ||
|
|
05a244d8fd | ||
|
|
019995078c | ||
|
|
f37cb6481c | ||
|
|
365f9a3923 | ||
|
|
bb4dd4fe35 | ||
|
|
d1ae531750 | ||
|
|
8ee2948f71 | ||
|
|
ab3fe79a7e | ||
|
|
32596758cc | ||
|
|
a5623ef7c3 | ||
|
|
9933484558 | ||
|
|
78fc28558c | ||
|
|
1e752dc3d2 | ||
|
|
6730c6ed67 | ||
|
|
9f8942a921 | ||
|
|
33c68065d5 | ||
|
|
9ca14f200d | ||
|
|
eae5002b69 | ||
|
|
b9e62bfceb | ||
|
|
f60122c64a | ||
|
|
c030ccc4f8 | ||
|
|
0076a85fdb | ||
|
|
c3d37a460c | ||
|
|
c04c13b2f8 | ||
|
|
4b4bed59cd | ||
|
|
4a0fb59461 | ||
|
|
f5293ca645 | ||
|
|
b2ff2d8cee | ||
|
|
8d3b156738 | ||
|
|
00f39a2007 | ||
|
|
4ac30116a9 | ||
|
|
e91096c535 | ||
|
|
e607ab7a3e | ||
|
|
67fdb77339 | ||
|
|
4f118c5f98 | ||
|
|
33ace85f7d | ||
|
|
33c50100ab | ||
|
|
782cbfc77f | ||
|
|
bbfc301440 | ||
|
|
98ec59df96 | ||
|
|
e13a37c193 | ||
|
|
0c3aabf77a | ||
|
|
d9880c4de8 | ||
|
|
0a4eb78ef0 | ||
|
|
daa1b5a313 | ||
|
|
d78f6a1f9e | ||
|
|
839fe8b96d | ||
|
|
25661ddff5 | ||
|
|
55eb3be5d5 | ||
|
|
0714570242 | ||
|
|
69ce0e565b | ||
|
|
b145bff566 | ||
|
|
a158056b99 | ||
|
|
e8479941c8 | ||
|
|
6d5ad9e229 | ||
|
|
055426ab53 | ||
|
|
4bafe97167 | ||
|
|
8aef6cd416 | ||
|
|
adb8f9bdec | ||
|
|
e9e69d9d17 | ||
|
|
2c5ccc0b1b | ||
|
|
ea58b51619 | ||
|
|
bfda0b62da | ||
|
|
b0a8e0d942 | ||
|
|
2161b2511c | ||
|
|
1b2a9c54b6 | ||
|
|
209e2c5bff | ||
|
|
820882db28 | ||
|
|
d64239f1ad | ||
|
|
2066fff994 | ||
|
|
e0b7d2f739 | ||
|
|
cd66b7d70c | ||
|
|
dc74f5816f | ||
|
|
c67fcf0e45 | ||
|
|
073b117528 | ||
|
|
ef6bc4c3ae | ||
|
|
468121a453 | ||
|
|
4679d321de | ||
|
|
c9af15b320 | ||
|
|
9b5bb1b07a | ||
|
|
aaa6573272 | ||
|
|
741ecac78b | ||
|
|
1dd096213e | ||
|
|
ed44244291 | ||
|
|
0b22c41d55 | ||
|
|
25c6ee40f7 | ||
|
|
cdc102af07 | ||
|
|
b9f0af0e79 | ||
|
|
86821f4606 | ||
|
|
0ef9c4db10 | ||
|
|
c101b85054 | ||
|
|
fe3b00292a | ||
|
|
10fe113099 | ||
|
|
1bf5f10f53 | ||
|
|
e2bbcaabbd | ||
|
|
4265915201 | ||
|
|
7e3c511027 |
@@ -3,12 +3,14 @@ data
|
||||
docker
|
||||
docs
|
||||
tests
|
||||
node_modules
|
||||
|
||||
lnbits/static/css/*
|
||||
lnbits/static/bundle.js
|
||||
lnbits/static/bundle.css
|
||||
|
||||
*.md
|
||||
!README.md
|
||||
*.log
|
||||
|
||||
.env
|
||||
@@ -21,3 +23,10 @@ mypy.ini
|
||||
package-lock.json
|
||||
package.json
|
||||
pytest.ini
|
||||
|
||||
.mypy_cache
|
||||
.github
|
||||
.pytest_cache
|
||||
.vscode
|
||||
bin
|
||||
dist
|
||||
|
||||
@@ -1,39 +1,133 @@
|
||||
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
||||
|
||||
######################################
|
||||
########### Admin Settings ###########
|
||||
###### .env ONLY SETTINGS ############
|
||||
######################################
|
||||
# The following settings are ONLY set in your .env file.
|
||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||
|
||||
# === First Install Token ===
|
||||
# If set the user is required to enter this token on the /first_install page
|
||||
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||
|
||||
# === Security ===
|
||||
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||
AUTH_HTTPS_ONLY=true
|
||||
|
||||
# === Logging and Development ===
|
||||
|
||||
DEBUG=False
|
||||
DEBUG_DATABASE=False
|
||||
BUNDLE_ASSETS=True
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
ENABLE_LOG_TO_FILE=true
|
||||
|
||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||
LOG_ROTATION="100 MB"
|
||||
LOG_RETENTION="3 months"
|
||||
# for database cleanup commands
|
||||
# CLEANUP_WALLETS_DAYS=90
|
||||
# Hard limit for total created users. Set to 0 to disable the limit.
|
||||
# LNBITS_MAX_USERS=0
|
||||
# Hard limit for total installed extensions. Set to 0 to disable the limit.
|
||||
# LNBITS_MAX_EXTENSIONS=0
|
||||
|
||||
# === Admin Settings ===
|
||||
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
|
||||
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the
|
||||
# configurations defined in `ReadOnlySettings` will still be read from the environment variables.
|
||||
# The rest of the settings will be stored in your database and you will be able to change them
|
||||
# only through the Admin UI.
|
||||
# Disable this to make LNbits use this config file again.
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
|
||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
# Disable this and clear `settings` table from database to make LNbits use this config file again.
|
||||
LNBITS_ADMIN_UI=true
|
||||
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
# VERSION=
|
||||
# USER_AGENT=
|
||||
|
||||
# === LNbits ===
|
||||
|
||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||
# for both PostgreSQL and CockroachDB, you'll need to install
|
||||
# psycopg2 as an additional dependency
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||
|
||||
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
|
||||
# The extension must be removed from this list in order to not be re-installed.
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
||||
|
||||
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
|
||||
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
|
||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, SparkL2Wallet"
|
||||
|
||||
# uvicorn variable, allow https behind a proxy
|
||||
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
||||
# http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https
|
||||
FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
# Path where extensions will be installed (defaults to `./lnbits/`).
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
# ID of the super user. The user ID must exist.
|
||||
# SUPER_USER=""
|
||||
|
||||
# LNBITS_TITLE="LNbits API"
|
||||
# LNBITS_PATH="folder/path"
|
||||
|
||||
# === Auth Configurations ===
|
||||
|
||||
# Secret Key: will default to the hash of the super user.
|
||||
# !!!!! It is strongly recommended that you set your own strong random value !!!!
|
||||
AUTH_SECRET_KEY=""
|
||||
|
||||
# === Funding Source ===# How many times to retry connectiong to the Funding Source before defaulting to the VoidWallet
|
||||
# FUNDING_SOURCE_MAX_RETRIES=4
|
||||
|
||||
######################################
|
||||
###### END .env ONLY SETTINGS ########
|
||||
######################################
|
||||
|
||||
######################################
|
||||
####### Auth Configurations ##########
|
||||
######################################
|
||||
|
||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
||||
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth, oidc-auth
|
||||
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||
# Set this flag if HTTP is used for OAuth
|
||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||
|
||||
######################################
|
||||
########### Admin Settings ###########
|
||||
######################################
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
|
||||
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
|
||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
|
||||
######################################
|
||||
########## Funding Source ############
|
||||
######################################
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, OpenNodeWallet"
|
||||
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
# just so you can see the UI before dealing with this file.
|
||||
|
||||
|
||||
# Invoice expiry for LND, CLN, Eclair, LNbits funding sources
|
||||
LIGHTNING_INVOICE_EXPIRY=3600
|
||||
|
||||
@@ -46,6 +140,20 @@ CLICHE_ENDPOINT=ws://127.0.0.1:12000
|
||||
SPARK_URL=http://localhost:9737/rpc
|
||||
SPARK_TOKEN=myaccesstoken
|
||||
|
||||
#CLNRest (using runes)
|
||||
CLNREST_URL=https://127.0.0.1:3010
|
||||
CLNREST_CA=/home/lightningd/.lightning/bitcoin/ca.pem
|
||||
CLNREST_CERT=/home/lightningd/.lightning/bitcoin/server.pem
|
||||
# CLNREST_CA = cat ca.pem | awk '{printf "%s\\n", $0} END {printf "\n"}'
|
||||
# CLNREST_CERT = cat server.pem | awk '{printf "%s\\n", $0} END {printf "\n"}'
|
||||
|
||||
CLNREST_READONLY_RUNE=lightning-cli createrune restrictions='[["method=listfunds", "method=listpays", "method=listinvoices", "method=getinfo", "method=summary", "method=waitanyinvoice"]]' | jq -r .rune
|
||||
CLNREST_INVOICE_RUNE=lightning-cli createrune restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' | jq -r .rune
|
||||
CLNREST_PAY_RUNE=lightning-cli createrune restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune
|
||||
#CLNREST_RENEPAY_RUNE=lightning-cli createrune restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune
|
||||
#CLNREST_LAST_PAY_INDEX='lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max'
|
||||
#CLNREST_NODEID=lightning-cli getinfo | jq -r .id # only required for v23.08
|
||||
|
||||
# CoreLightningWallet
|
||||
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
|
||||
@@ -55,7 +163,7 @@ CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXS
|
||||
CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert"
|
||||
|
||||
# LnbitsWallet
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_ENDPOINT=https://demo.lnbits.com
|
||||
LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||
|
||||
# LndWallet
|
||||
@@ -84,10 +192,39 @@ LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
|
||||
ALBY_API_ENDPOINT=https://api.getalby.com/
|
||||
ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
||||
|
||||
# BoltzWallet
|
||||
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
||||
# HEXSTRING instead of path also possible
|
||||
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||
# HEXSTRING instead of path also possible
|
||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||
|
||||
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
|
||||
SPARK_L2_NETWORK=MAINNET
|
||||
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
|
||||
SPARK_L2_EXTERNAL_API_KEY=
|
||||
# optional tuning
|
||||
# SPARK_L2_PAY_WAIT_MS=4000
|
||||
# SPARK_L2_PAY_POLL_MS=500
|
||||
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
|
||||
|
||||
# StrikeWallet
|
||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
||||
|
||||
# ZBDWallet
|
||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||
|
||||
# BlinkWallet
|
||||
BLINK_API_ENDPOINT=https://api.blink.sv/graphql
|
||||
BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql
|
||||
BLINK_TOKEN=BLINK_TOKEN
|
||||
|
||||
# PhoenixdWallet
|
||||
PHOENIXD_API_ENDPOINT=http://localhost:9740/
|
||||
PHOENIXD_API_PASSWORD=PHOENIXD_KEY
|
||||
|
||||
# OpenNodeWallet
|
||||
OPENNODE_API_ENDPOINT=https://api.opennode.com/
|
||||
OPENNODE_KEY=OPENNODE_ADMIN_KEY
|
||||
@@ -100,21 +237,29 @@ LNBITS_DENOMINATION=sats
|
||||
ECLAIR_URL=http://127.0.0.1:8283
|
||||
ECLAIR_PASS=eclairpw
|
||||
|
||||
# NWCWalllet
|
||||
NWC_PAIRING_URL="nostr+walletconnect://000...000?relay=example.com&secret=123"
|
||||
|
||||
# LnTipsWallet
|
||||
# Enter /api in LightningTipBot to get your key
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
||||
######################################
|
||||
####### Auth Configurations ##########
|
||||
######################################
|
||||
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
|
||||
AUTH_SECRET_KEY=""
|
||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
||||
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth
|
||||
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||
# Set this flag if HTTP is used for OAuth
|
||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||
# BreezSdkWallet
|
||||
BREEZ_API_KEY=KEY
|
||||
BREEZ_GREENLIGHT_SEED=SEED
|
||||
# A Greenlight invite code or Greenlight partner certificate/key can be used
|
||||
BREEZ_GREENLIGHT_INVITE_CODE=CODE
|
||||
BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRING
|
||||
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
|
||||
# BREEZ_USE_TRAMPOLINE=true
|
||||
|
||||
# BreezLiquidSdkWallet
|
||||
# get your own api key here https://breez.technology/request-api-key/#contact-us-form-sdk
|
||||
# or keep the api key empty to use the LNbits key for referrals (API key is not a secret)
|
||||
# BREEZ_LIQUID_API_KEY=""
|
||||
BREEZ_LIQUID_SEED="MNEMONIC SEED PHRASE"
|
||||
# BREEZ_LIQUID_FEE_OFFSET_SAT=50
|
||||
|
||||
# Google OAuth Config
|
||||
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
||||
@@ -131,13 +276,56 @@ GITHUB_CLIENT_SECRET=""
|
||||
KEYCLOAK_CLIENT_ID=""
|
||||
KEYCLOAK_CLIENT_SECRET=""
|
||||
KEYCLOAK_DISCOVERY_URL=""
|
||||
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
||||
KEYCLOAK_CLIENT_CUSTOM_ICON=""
|
||||
|
||||
# OIDC OAuth Config
|
||||
# Generic OIDC provider configuration
|
||||
# Make sure that the redirect URI in your OIDC provider is set to: https://{domain}/api/v1/auth/oidc/token
|
||||
# Required scopes: openid, email, profile
|
||||
# The discovery URL must be accessible from your LNbits server
|
||||
# Always use HTTPS in production environments
|
||||
# The CUSTOM_ORG and CUSTOM_ICON settings allow you to customize the login button
|
||||
# For example: "Login via Zitadel" with the Zitadel logo
|
||||
OIDC_DISCOVERY_URL=""
|
||||
OIDC_CLIENT_ID=""
|
||||
OIDC_CLIENT_SECRET=""
|
||||
OIDC_CLIENT_CUSTOM_ORG=""
|
||||
OIDC_CLIENT_CUSTOM_ICON=""
|
||||
|
||||
# Example OIDC configurations for various providers:
|
||||
#
|
||||
# ZITADEL:
|
||||
# OIDC_DISCOVERY_URL=https://login.yourdomain.de/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-zitadel-client-id@project-id
|
||||
# OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Zitadel
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/zitadel.png
|
||||
#
|
||||
# AUTHENTIK:
|
||||
# OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/lnbits/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-authentik-client-id
|
||||
# OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Authentik
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authentik.png
|
||||
#
|
||||
# AUTHELIA:
|
||||
# OIDC_DISCOVERY_URL=https://auth.yourdomain.com/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-authelia-client-id
|
||||
# OIDC_CLIENT_SECRET=your-authelia-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Authelia
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authelia.png
|
||||
#
|
||||
# OKTA:
|
||||
# OIDC_DISCOVERY_URL=https://your-domain.okta.com/.well-known/openid-configuration
|
||||
# OIDC_CLIENT_ID=your-okta-client-id
|
||||
# OIDC_CLIENT_SECRET=your-okta-client-secret
|
||||
# OIDC_CLIENT_CUSTOM_ORG=Okta
|
||||
# OIDC_CLIENT_CUSTOM_ICON=/static/images/okta.png
|
||||
|
||||
|
||||
######################################
|
||||
|
||||
# uvicorn variable, uncomment to allow https behind a proxy
|
||||
# FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
# Server security, rate limiting ips, blocked ips, allowed ips
|
||||
LNBITS_RATE_LIMIT_NO="200"
|
||||
LNBITS_RATE_LIMIT_UNIT="minute"
|
||||
@@ -148,11 +336,12 @@ LNBITS_BLOCKED_IPS=""
|
||||
# if set new users will not be able to create accounts
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# ID of the super user. The user ID must exist.
|
||||
# SUPER_USER=""
|
||||
|
||||
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
|
||||
# Extensions enabled by default when a user is created
|
||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||
|
||||
# Start LNbits core only. The extensions are not loaded.
|
||||
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
|
||||
@@ -173,30 +362,13 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
# LNBITS_AD_SPACE_TITLE="Supported by"
|
||||
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
|
||||
# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png"
|
||||
# LNBITS_SHOW_HOME_PAGE_ELEMENTS=true # if set to true, the ad space will be displayed on the home page
|
||||
# LNBITS_CUSTOM_BADGE="USE WITH CAUTION - LNbits wallet is still in BETA"
|
||||
# LNBITS_CUSTOM_BADGE_COLOR="warning"
|
||||
|
||||
# Hides wallet api, extensions can choose to honor
|
||||
LNBITS_HIDE_API=false
|
||||
|
||||
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
|
||||
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
|
||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Path where extensions will be installed (defaults to `./lnbits/`).
|
||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||
|
||||
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
|
||||
# The extension must be removed from this list in order to not be re-installed.
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
||||
|
||||
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
|
||||
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
|
||||
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
|
||||
# for both PostgreSQL and CockroachDB, you'll need to install
|
||||
# psycopg2 as an additional dependency
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||
|
||||
# the service fee (in percent)
|
||||
LNBITS_SERVICE_FEE=0.0
|
||||
# the wallet where fees go to
|
||||
@@ -206,10 +378,12 @@ LNBITS_SERVICE_FEE=0.0
|
||||
# disable fees for internal transactions
|
||||
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
|
||||
|
||||
# value in millisats
|
||||
# The minimum fee reserved per payment (millisats)
|
||||
LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
# The percentage of the payment amount to reserve for routing fees (percent)
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
# The default time to wait to for status response from the funding source when paying an invoice
|
||||
LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS=5
|
||||
|
||||
# limit the maximum balance for each wallet
|
||||
# throw an error if the wallet attempts to create a new invoice
|
||||
@@ -224,17 +398,3 @@ LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
######################################
|
||||
###### Logging and Development #######
|
||||
######################################
|
||||
|
||||
DEBUG=false
|
||||
DEBUG_DATABASE=false
|
||||
BUNDLE_ASSETS=true
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
ENABLE_LOG_TO_FILE=true
|
||||
|
||||
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
|
||||
LOG_ROTATION="100 MB"
|
||||
LOG_RETENTION="3 months"
|
||||
|
||||
# for database cleanup commands
|
||||
# CLEANUP_WALLETS_DAYS=90
|
||||
|
||||
@@ -1 +1 @@
|
||||
custom: https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
|
||||
custom: https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg
|
||||
|
||||
@@ -4,13 +4,10 @@ inputs:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
default: "1.7.0"
|
||||
default: "3.10"
|
||||
node-version:
|
||||
description: "Node Version"
|
||||
default: "18.x"
|
||||
default: "20.x"
|
||||
npm:
|
||||
description: "use npm"
|
||||
default: false
|
||||
@@ -21,40 +18,27 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
# cache poetry install via pip
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry ${{ inputs.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
poetry-version: ${{ inputs.poetry-version }}
|
||||
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
shell: bash
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
enable-cache: true
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Install the project dependencies
|
||||
shell: bash
|
||||
run: poetry install
|
||||
run: uv sync --locked --all-extras --dev
|
||||
|
||||
- name: Use Node.js ${{ inputs.node-version }}
|
||||
if: ${{ (inputs.npm == 'true') }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
if: ${{ (inputs.npm == 'true') }}
|
||||
name: Define a cache for the npm based on the dependencies lock file
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
LAUNCH_DIR="$PWD"
|
||||
|
||||
# Define persistent storage for extracted LNbits
|
||||
PERSISTENT_DIR="$HOME/.local/share/lnbits"
|
||||
|
||||
# Remove existing LNbits directory before extraction
|
||||
if [ -d "$PERSISTENT_DIR" ]; then
|
||||
echo "Removing existing LNbits directory..."
|
||||
rm -rf "$PERSISTENT_DIR"
|
||||
fi
|
||||
|
||||
# Ensure the persistent directory exists
|
||||
mkdir -p "$PERSISTENT_DIR"
|
||||
|
||||
# Extract LNbits from the AppImage if not already extracted
|
||||
echo "Extracting LNbits to disk for better performance..."
|
||||
cp -r "$APPDIR/usr/lnbits"/* "$PERSISTENT_DIR/"
|
||||
chmod +x "$PERSISTENT_DIR/dist/lnbits"
|
||||
|
||||
# Check if the directory exists, and create it if it doesn't
|
||||
if [ ! -d "$LAUNCH_DIR/lnbits/database" ]; then
|
||||
mkdir -p "$LAUNCH_DIR/lnbits/database"
|
||||
echo "Created database directory at $LAUNCH_DIR/lnbits/database"
|
||||
fi
|
||||
|
||||
if [ ! -d "$LAUNCH_DIR/lnbits/extensions" ]; then
|
||||
mkdir -p "$LAUNCH_DIR/lnbits/extensions"
|
||||
echo "Created extensions directory at $LAUNCH_DIR/lnbits/extensions"
|
||||
fi
|
||||
|
||||
cd "$PERSISTENT_DIR"
|
||||
|
||||
# Export the directory as an environment variable for the app
|
||||
LNBITS_DATA_FOLDER="${LNBITS_DATA_FOLDER:-$LAUNCH_DIR/lnbits/database}"
|
||||
LNBITS_EXTENSIONS_PATH="${LNBITS_EXTENSIONS_PATH:-$LAUNCH_DIR/lnbits/extensions}"
|
||||
export LNBITS_DATA_FOLDER
|
||||
export LNBITS_EXTENSIONS_PATH
|
||||
|
||||
# Define the LNbits URL
|
||||
URL="http://0.0.0.0:5000"
|
||||
|
||||
"./dist/lnbits" "$@" &
|
||||
LNBITS_PID=$!
|
||||
|
||||
# Wait for LNbits to be ready before showing the popup
|
||||
sleep 3
|
||||
CLOSED=false
|
||||
|
||||
# Function to stop LNbits gracefully
|
||||
kill_lnbits() {
|
||||
LN_PIDS=$(lsof -t -i:5000 2>/dev/null) # Capture all PIDs
|
||||
if [ -n "$LN_PIDS" ]; then
|
||||
echo "Stopping LNbits (PIDs: $LN_PIDS)..."
|
||||
kill -2 $LN_PIDS # Send SIGINT to all processes on port 5000
|
||||
CLOSED=true
|
||||
fi
|
||||
}
|
||||
|
||||
# Show a GUI with a clickable link to open the browser
|
||||
if command -v zenity >/dev/null 2>&1; then
|
||||
while [ "$CLOSED" = false ]; do
|
||||
zenity --info --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --ok-label="Close Server"
|
||||
kill_lnbits
|
||||
sleep 1
|
||||
done
|
||||
elif command -v yad >/dev/null 2>&1; then
|
||||
while [ "$CLOSED" = false ]; do
|
||||
yad --title="LNbits" --width=400 --text="<b>LNbits is running.</b>\n\n<a href='$URL'>$URL</a>\n\nClick 'Close Server' to stop LNbits." --button="Close Server":0
|
||||
kill_lnbits
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "No GUI tool found. LNbits is running at $URL"
|
||||
fi
|
||||
|
||||
# Ensure the script doesn't hang after closing
|
||||
if ps -p $LNBITS_PID >/dev/null 2>&1; then
|
||||
wait $LNBITS_PID 2>/dev/null || true
|
||||
fi
|
||||
@@ -0,0 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=LNbits
|
||||
Exec=lnbits
|
||||
Icon=lnbits
|
||||
Type=Application
|
||||
Categories=X-Bitcoin;X-LightningNetwork;X-Finance;Network;
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,123 @@
|
||||
name: Build LNbits AppImage
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'The tag name for the release'
|
||||
required: true
|
||||
type: string
|
||||
upload_url:
|
||||
description: 'The upload URL for the release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'The tag name for the release'
|
||||
required: true
|
||||
type: string
|
||||
upload_url:
|
||||
description: 'The upload URL for the release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-linux-package:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install system deps and uv
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Cache uv and venv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
.venv
|
||||
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
||||
|
||||
- name: Prepare packaging & clone LNbits
|
||||
run: |
|
||||
mv .github/packaging packaging
|
||||
mkdir -p packaging/linux/AppDir/usr
|
||||
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
||||
shell: bash
|
||||
|
||||
- name: Build LNbits binary (uv + PyInstaller)
|
||||
run: |
|
||||
cd packaging/linux/AppDir/usr/lnbits
|
||||
uv sync --all-extras --no-dev
|
||||
uv pip install pyinstaller
|
||||
uv run pyinstaller \
|
||||
--onefile \
|
||||
--name lnbits \
|
||||
--hidden-import=embit \
|
||||
--hidden-import=bitstring.bitstore_bitarray \
|
||||
--collect-all embit \
|
||||
--collect-all bitstring \
|
||||
--collect-all bitarray \
|
||||
--collect-all lnbits \
|
||||
--collect-all sqlalchemy \
|
||||
--collect-all breez_sdk \
|
||||
--collect-binaries breez_sdk \
|
||||
--collect-all breez_sdk_liquid \
|
||||
--collect-binaries breez_sdk_liquid \
|
||||
--collect-all aiosqlite \
|
||||
--hidden-import=passlib.handlers.bcrypt \
|
||||
"$(uv run which lnbits)"
|
||||
|
||||
cd ../../../../..
|
||||
chmod +x packaging/linux/AppDir/AppRun
|
||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
||||
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
|
||||
|
||||
# keep AppDir slim
|
||||
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
||||
! -name 'dist' \
|
||||
! -name 'lnbits' \
|
||||
-exec rm -rf {} +
|
||||
|
||||
# Build AppImage
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
TAG_NAME=${{ inputs.tag_name }}
|
||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||
./appimagetool-x86_64.AppImage \
|
||||
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
||||
packaging/linux/AppDir "$APPIMAGE_NAME"
|
||||
chmod +x "$APPIMAGE_NAME"
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
|
||||
# 🔎 quick audit: show glibc versions referenced by the binary
|
||||
echo "Runner glibc:"
|
||||
ldd --version | head -n1 || true
|
||||
echo "Symbols needed by lnbits:"
|
||||
strings "$APPIMAGE_NAME" | grep -o 'GLIBC_[0-9.]*' | sort -u || true
|
||||
shell: bash
|
||||
|
||||
- name: Upload Linux Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload_url }}
|
||||
asset_path: ${{ env.APPIMAGE_NAME }}
|
||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,29 @@
|
||||
name: bundle
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
bundle:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||
with:
|
||||
python-version: "3.10"
|
||||
node-version: "24.x"
|
||||
npm: true
|
||||
- run: make bundle
|
||||
- name: Commit and push bundle changes
|
||||
run: |
|
||||
git config user.name "alan"
|
||||
git config user.email "alan@lnbits.com"
|
||||
git add lnbits/static
|
||||
if git diff --cached --quiet; then
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore: make bundle [skip ci]"
|
||||
git push
|
||||
@@ -1,36 +1,62 @@
|
||||
name: LNbits CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
|
||||
tests:
|
||||
test-api:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
custom-pytest: "uv run pytest tests/api"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
migrations:
|
||||
test-wallets:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
make: test-migration
|
||||
db-name: migration
|
||||
custom-pytest: "uv run pytest tests/wallets"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
test-unit:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.12"]
|
||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
custom-pytest: "uv run pytest tests/unit"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
db-url: ${{ matrix.db-url }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
migration:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.12"]
|
||||
uses: ./.github/workflows/migration.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
openapi:
|
||||
needs: [ lint ]
|
||||
@@ -43,18 +69,31 @@ jobs:
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
|
||||
python-version: ["3.12"]
|
||||
backend-wallet-class:
|
||||
- BoltzWallet
|
||||
- LndRestWallet
|
||||
- LndWallet
|
||||
- CoreLightningWallet
|
||||
- CoreLightningRestWallet
|
||||
- LNbitsWallet
|
||||
- EclairWallet
|
||||
with:
|
||||
custom-pytest: "uv run pytest tests/regtest"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
backend-wallet-class: ${{ matrix.backend-wallet-class }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
jmeter:
|
||||
needs: [ lint ]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.5.1"]
|
||||
python-version: ["3.12"]
|
||||
uses: ./.github/workflows/jmeter.yml
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
|
||||
bundle:
|
||||
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
|
||||
uses: ./.github/workflows/bundle.yml
|
||||
|
||||
@@ -10,10 +10,10 @@ on:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- run: git checkout HEAD^2
|
||||
|
||||
@@ -11,10 +11,15 @@ on:
|
||||
tag:
|
||||
default: latest
|
||||
type: string
|
||||
secrets:
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
push_to_dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -47,12 +52,24 @@ jobs:
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Build and push latest tag
|
||||
- name: Build and push boltz
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
context: docker/boltzclient
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||
|
||||
- name: Build and push sparkl2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: docker/sparkl2
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: JMeter Tests
|
||||
name: JMeter Extension Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -6,93 +6,60 @@ on:
|
||||
python-version:
|
||||
description: "Python Version"
|
||||
required: true
|
||||
default: "3.9"
|
||||
type: string
|
||||
poetry-version:
|
||||
description: "Poetry Version"
|
||||
required: true
|
||||
default: "1.5.1"
|
||||
default: "3.10"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
action_build:
|
||||
runs-on: ubuntu-latest
|
||||
jmeter:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry ${{ inputs.poetry-version }}
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ inputs.poetry-version }}
|
||||
|
||||
- name: create logs and reports dir
|
||||
run: |
|
||||
mkdir logs
|
||||
mkdir reports
|
||||
|
||||
- name: install packages
|
||||
run: poetry install
|
||||
|
||||
- name: run LNbits
|
||||
env:
|
||||
LNBITS_ADMIN_UI: true
|
||||
AUTH_HTTPS_ONLY: false
|
||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: |
|
||||
poetry run lnbits > logs/lnbits.log &
|
||||
sleep 5
|
||||
uv run lnbits &
|
||||
sleep 10
|
||||
|
||||
|
||||
- name: install jmeter
|
||||
- name: setup java version
|
||||
run: |
|
||||
update-java-alternatives --list
|
||||
sudo update-java-alternatives --set /usr/lib/jvm/temurin-8-jdk-amd64
|
||||
java -version
|
||||
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.2.zip
|
||||
unzip apache-jmeter-5.6.2.zip
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -v
|
||||
|
||||
- name: start mirror server
|
||||
- name: clone lnbits-extensions, install jmeter and run tests
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
|
||||
EXTENSIONS_MANIFEST_PATH: "/lnbits/lnbits-extensions/refs/heads/main/extensions.json"
|
||||
run: |
|
||||
libs_dir=$GITHUB_WORKSPACE/apache-jmeter-5.6.2/lib/
|
||||
echo "Fix bad Jmeter lib names. Lib dir: $libs_dir"
|
||||
git clone https://github.com/lnbits/lnbits-extensions
|
||||
cd lnbits-extensions
|
||||
mkdir logs
|
||||
mkdir reports
|
||||
make install-jmeter
|
||||
make start-mirror-server
|
||||
make test
|
||||
|
||||
mv $libs_dir/slf4j-api-1.7.36.jar $libs_dir/slf4j-api-1.7.25.jar
|
||||
mv $libs_dir/log4j-slf4j-impl-2.20.0.jar $libs_dir/log4j-slf4j-impl-2.11.0.jar
|
||||
mv $libs_dir/log4j-api-2.20.0.jar $libs_dir/log4j-api-2.11.1.jar
|
||||
mv $libs_dir/log4j-core-2.20.0.jar $libs_dir/log4j-core-2.11.1.jar
|
||||
mv $libs_dir/log4j-1.2-api-2.20.0.jar $libs_dir/og4j-1.2-api-2.11.1.jar
|
||||
|
||||
echo "Starting the mirror server on dfault port 8081."
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/mirror-server &
|
||||
|
||||
- name: run jmx scripts
|
||||
- name: print lnbits log
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
for file in $( ls $GITHUB_WORKSPACE/integration/*.jmx); do
|
||||
echo "Running test with $file"
|
||||
filename=$(basename "$file" ".jmx")
|
||||
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -n -t $file -l logs/$filename.log -e -o reports ;
|
||||
error_count=$(cat jmeter.log | grep "summary =" | awk '{print $19}')
|
||||
echo "Error count: $error_count"
|
||||
if [[ "$error_count" == "0" ]]; then
|
||||
echo "Test $filename OK."
|
||||
rm -r reports/*
|
||||
else
|
||||
echo "Test $filename failed. Error count: $error_count."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# catch up time for lnbits
|
||||
sleep 1
|
||||
cat data/logs/debug.log
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: upload jmeter test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: jmeter-test-results
|
||||
name: jmeter-extension-test-results
|
||||
path: |
|
||||
reports/
|
||||
logs/
|
||||
|
||||
lnbits-extensions/reports/
|
||||
lnbits-extensions/logs/
|
||||
|
||||
@@ -6,50 +6,30 @@ jobs:
|
||||
|
||||
black:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: checkblack
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
ruff:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: checkruff
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
mypy:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: mypy
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
pyright:
|
||||
uses: ./.github/workflows/make.yml
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
with:
|
||||
make: pyright
|
||||
python-version: ${{ matrix.python-version }}
|
||||
npm: true
|
||||
|
||||
|
||||
prettier:
|
||||
uses: ./.github/workflows/make.yml
|
||||
with:
|
||||
make: checkprettier
|
||||
npm: true
|
||||
|
||||
bundle:
|
||||
uses: ./.github/workflows/make.yml
|
||||
with:
|
||||
make: checkbundle
|
||||
npm: true
|
||||
poetry:
|
||||
uses: ./.github/workflows/poetry.yml
|
||||
|
||||
@@ -14,19 +14,19 @@ on:
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
default: "3.12"
|
||||
|
||||
jobs:
|
||||
make:
|
||||
name: ${{ inputs.make }} (${{ inputs.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-latest"]
|
||||
node-version: ["18.x"]
|
||||
os-version: ["ubuntu-24.04"]
|
||||
node-version: ["24.x"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/prepare
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: migration
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
|
||||
jobs:
|
||||
make:
|
||||
name: migration (${{ inputs.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os-version: ["ubuntu-24.04"]
|
||||
runs-on: ${{ matrix.os-version }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: lnbits
|
||||
POSTGRES_PASSWORD: lnbits
|
||||
POSTGRES_DB: migration
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- run: make test-migration
|
||||
@@ -14,25 +14,24 @@ on:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
- 'uv.lock'
|
||||
- '.github/workflows/nix.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
- 'uv.lock'
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v24
|
||||
- uses: cachix/install-nix-action@v27
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-23.11
|
||||
- uses: cachix/cachix-action@v13
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix'
|
||||
nix_path: nixpkgs=channel:nixos-24.05
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: lnbits
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: poetry
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python-version:
|
||||
description: "python version"
|
||||
type: string
|
||||
default: "3.10"
|
||||
|
||||
jobs:
|
||||
poetry:
|
||||
name: run poetry install to check lock file
|
||||
runs-on: "ubuntu-24.04"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: install poetry
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry install
|
||||
@@ -3,79 +3,88 @@ name: regtest
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
make:
|
||||
default: test
|
||||
custom-pytest:
|
||||
description: "Custom pytest arguments"
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.9"
|
||||
default: "3.12"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-latest"
|
||||
default: "ubuntu-24.04"
|
||||
type: string
|
||||
backend-wallet-class:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
regtest:
|
||||
runs-on: ${{ inputs.os-version }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
- name: docker build
|
||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: lnbits/lnbits:latest
|
||||
cache-from: type=registry,ref=lnbits/lnbits:latest
|
||||
cache-to: type=inline
|
||||
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
chmod +x ./tests
|
||||
./tests
|
||||
sudo chmod -R a+rwx .
|
||||
docker build -t lnbits/lnbits .
|
||||
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Create fake admin
|
||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
||||
run: docker exec lnbits-lnbits-1 poetry run python tools/create_fake_admin.py
|
||||
- name: Setup Regtest
|
||||
run: |
|
||||
cd docker/regtest
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest
|
||||
sudo chmod -R a+rwx .
|
||||
|
||||
- name: Run Tests
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
env:
|
||||
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
|
||||
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
|
||||
LND_REST_ENDPOINT: https://localhost:8081/
|
||||
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
|
||||
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_GRPC_ENDPOINT: localhost
|
||||
LND_GRPC_PORT: 10009
|
||||
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
|
||||
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
||||
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
|
||||
CORELIGHTNING_REST_URL: https://localhost:3001
|
||||
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
|
||||
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
|
||||
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
|
||||
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
|
||||
LNBITS_ENDPOINT: http://localhost:5001
|
||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||
ECLAIR_URL: http://127.0.0.1:8082
|
||||
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
|
||||
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
||||
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
|
||||
ECLAIR_PASS: lnbits
|
||||
run: make test-real-wallet
|
||||
PYTHONUNBUFFERED: 1
|
||||
DEBUG: true
|
||||
with:
|
||||
verbose: true
|
||||
job-summary: true
|
||||
emoji: false
|
||||
click-to-expand: true
|
||||
custom-pytest: ${{ inputs.custom-pytest }}
|
||||
report-title: "regtest (${{ inputs.python-version }}, ${{ inputs.backend-wallet-class }}"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
- name: docker lnbits logs
|
||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
||||
run: |
|
||||
docker logs lnbits-lnbits-1
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: release-rc
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*-rc[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create github pre-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$tag" --prerelease --generate-notes --draft
|
||||
- id: get_upload_url
|
||||
name: Get upload url of Github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
|
||||
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest-rc:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest-rc
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Build the project
|
||||
run: uv build
|
||||
- name: Publish to pypi
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||
run: uv publish
|
||||
|
||||
appimage:
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/appimage.yml
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
@@ -10,19 +10,79 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$tag" --generate-notes --draft
|
||||
- id: get_upload_url
|
||||
name: Get upload url of Github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
|
||||
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
docker-latest-rc:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
tag: latest-rc
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
pypi:
|
||||
if: github.repository == 'lnbits/lnbits'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Build the project
|
||||
run: uv build
|
||||
- name: Publish to pypi
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||
run: uv publish
|
||||
|
||||
appimage:
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/appimage.yml
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
|
||||
@@ -3,14 +3,15 @@ name: tests
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
make:
|
||||
default: test
|
||||
custom-pytest:
|
||||
description: "Custom pytest arguments"
|
||||
required: true
|
||||
type: string
|
||||
python-version:
|
||||
default: "3.9"
|
||||
default: "3.12"
|
||||
type: string
|
||||
os-version:
|
||||
default: "ubuntu-latest"
|
||||
default: "ubuntu-24.04"
|
||||
type: string
|
||||
db-url:
|
||||
default: ""
|
||||
@@ -18,6 +19,9 @@ on:
|
||||
db-name:
|
||||
default: "lnbits"
|
||||
type: string
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -39,20 +43,30 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
- name: Run Tests
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
env:
|
||||
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
run: make ${{ inputs.make }}
|
||||
PYTHONUNBUFFERED: 1
|
||||
DEBUG: true
|
||||
with:
|
||||
verbose: true
|
||||
job-summary: true
|
||||
emoji: false
|
||||
click-to-expand: true
|
||||
custom-pytest: ${{ inputs.custom-pytest }}
|
||||
report-title: "test (${{ inputs.python-version }}, ${{ inputs.db-url }})"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: false
|
||||
|
||||
@@ -6,11 +6,14 @@ __pycache__
|
||||
*$py.class
|
||||
.mypy_cache
|
||||
.vscode
|
||||
.codex
|
||||
*-lock.json
|
||||
.python-version
|
||||
|
||||
*.egg
|
||||
*.egg-info
|
||||
.coverage
|
||||
.coverage.*
|
||||
.pytest_cache
|
||||
.webassets-cache
|
||||
htmlcov
|
||||
@@ -34,10 +37,12 @@ __bundle__
|
||||
coverage.xml
|
||||
node_modules
|
||||
lnbits/static/bundle.js
|
||||
lnbits/static/bundle-components.js
|
||||
lnbits/static/bundle.css
|
||||
lnbits/static/bundle.min.js.old
|
||||
lnbits/static/bundle.min.css.old
|
||||
docker
|
||||
lnbits/static/bundle-components.min.js.old
|
||||
lnbits/upgrades
|
||||
|
||||
# Nix
|
||||
*result*
|
||||
@@ -48,5 +53,12 @@ fly.toml
|
||||
lnbits-backup.zip
|
||||
|
||||
# Ignore extensions (post installable extension PR)
|
||||
extensions
|
||||
upgrades/
|
||||
/lnbits/extensions
|
||||
/lnbits/upgrades/
|
||||
|
||||
# builded python package
|
||||
dist
|
||||
|
||||
# jetbrains
|
||||
.idea
|
||||
.venv
|
||||
|
||||
@@ -2,7 +2,7 @@ exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/exte
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -14,16 +14,16 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: check-case-conflict
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.2.0
|
||||
rev: 26.3.1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.2
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.7.4
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
|
||||
@@ -10,4 +10,9 @@
|
||||
|
||||
**/lnbits/static/vendor
|
||||
**/lnbits/static/bundle.*
|
||||
**/lnbits/static/bundle-components.*
|
||||
**/lnbits/static/css/*
|
||||
|
||||
flake.lock
|
||||
|
||||
.venv
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Feature Spec: [FEAT-XXX] - Short Descriptive Title
|
||||
|
||||
**Milestone:** [e.g. MVP Core / Performance & Polish / Extension Framework]
|
||||
**Priority:** Must-have / Should-have / Nice-to-have
|
||||
**Spec Owner:** [Your Name / AI Agent Name]
|
||||
**Status:** Draft → Under Review → Approved → Implemented → Verified
|
||||
|
||||
## 1. Purpose & User Story
|
||||
|
||||
As a [user type], I want [goal] so that [benefit].
|
||||
|
||||
_(One clear sentence. Keep it concise.)_
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
- [ ] REQ-1: [Clear, testable description]
|
||||
- [ ] REQ-2: ...
|
||||
- [ ] REQ-3: ...
|
||||
|
||||
_(List what the feature must do. Make each item verifiable.)_
|
||||
|
||||
## 3. Non-Functional Requirements
|
||||
|
||||
- **Performance:** [e.g. Latency < 800ms at p95, max 5k tokens, etc.]
|
||||
- **Security / Safety:** [e.g. Input validation, no raw errors to user, etc.]
|
||||
- **Compatibility:** [e.g. Works with all existing wallet backends, no breaking changes for extensions]
|
||||
- **UI/UX:** [e.g. Follows existing Quasar/Vue patterns in wallet.js]
|
||||
- **Other:** [cost, scalability, accessibility, etc.]
|
||||
|
||||
## 4. Technical Approach (Optional – recommended for complex features)
|
||||
|
||||
- Proposed solution: [e.g. Extend existing CRUD in lnbits/core/, new extension, middleware change, etc.]
|
||||
- Key files to modify: [list expected files]
|
||||
- New dependencies: [none / specific package + version]
|
||||
- Migration / Database changes: [yes/no + description]
|
||||
|
||||
## 5. Success Criteria & Verification
|
||||
|
||||
**Must pass all of these to be accepted:**
|
||||
|
||||
- [ ] All functional requirements (REQ-\*) implemented and tested
|
||||
- [ ] Non-functional requirements met (performance, security, etc.)
|
||||
- [ ] Relevant tests pass: `make test-unit`, `make test-api`, `make test-regtest` (as applicable)
|
||||
- [ ] `make check` passes (ruff, mypy, pyright, prettier, checkbundle)
|
||||
- [ ] Constitution compliance: All changes respect CONSTITUTION.md
|
||||
- [ ] Backward compatibility: No breakage for existing extensions or wallet backends
|
||||
- [ ] Documentation updated (if applicable: README, OpenAPI, inline comments)
|
||||
|
||||
**Additional Tests / Edge Cases:**
|
||||
|
||||
- [ ] Test invalid inputs / error paths
|
||||
- [ ] Test with FakeWallet and at least one real backend
|
||||
- [ ] Test with multiple extensions installed
|
||||
|
||||
## 6. Safety & Risk Assessment
|
||||
|
||||
- Potential risks: [e.g. Payment flow impact, key exposure, extension conflicts]
|
||||
- Mitigation: [how addressed]
|
||||
- Security review needed: [yes/no]
|
||||
|
||||
## 7. Implementation Notes (for AI / Developer)
|
||||
|
||||
- Style to match: Existing code patterns in `lnbits/core/` and `wallet.js`
|
||||
- Surgical changes only (per AGENTS.md)
|
||||
- Any known gotchas or dependencies on other features:
|
||||
|
||||
## 8. Acceptance Checklist (Sign-off)
|
||||
|
||||
- [ ] Spec reviewed and approved by project owner
|
||||
- [ ] Implementation completed
|
||||
- [ ] Verification steps passed
|
||||
- [ ] PR created with link to this spec
|
||||
- [ ] Constitution & AGENTS.md compliance confirmed
|
||||
|
||||
---
|
||||
|
||||
**Created:** [Date]
|
||||
**Last Updated:** [Date]
|
||||
**Approved By:** [Name / "Approved"]
|
||||
@@ -0,0 +1,122 @@
|
||||
# AGENTS.md - Instructions for All AI Coding Agents
|
||||
|
||||
This file is the **master instruction manual** for any AI agent (Grok, Claude, Cursor, Aider, etc.) working on LNbits.
|
||||
|
||||
## 1. Core Rule (Never Break This)
|
||||
|
||||
**You MUST read and strictly follow `CONSTITUTION.md` before doing any planning, coding, refactoring, or suggesting changes.**
|
||||
|
||||
- Every single change, feature, extension, or fix **must comply** with the Constitution.
|
||||
- If you detect a violation (in new code or existing code), you **must** flag it immediately and propose a fix or ask for clarification.
|
||||
- Constitution > any other instruction (including this file, user prompts, or previous conversations).
|
||||
|
||||
## 2. Mandatory Development Workflow
|
||||
|
||||
For **any non-trivial task** (new feature, bug fix, refactor, extension change):
|
||||
|
||||
1. **Constitution Check** – Re-read relevant sections of `CONSTITUTION.md`
|
||||
2. **Feature Spec Check** – If a spec exists in `.specify/`, follow it exactly. If none exists, ask the user for clarification or propose a minimal spec.
|
||||
3. **Think Step-by-Step** – Follow the "Think Before Coding" and "Simplicity First" guidelines below.
|
||||
4. **Surgical Changes** – Only touch what is necessary.
|
||||
5. **Implement**
|
||||
6. **Verify** – Run relevant tests (`make test-unit`, `make test-api`, etc.), `make check`, and confirm compliance.
|
||||
7. **Report** – Always include a clear summary.
|
||||
|
||||
Use the following response format:
|
||||
|
||||
```markdown
|
||||
## Constitution & Spec Compliance
|
||||
|
||||
- Relevant Constitution sections checked: [list or quote key rules]
|
||||
- Feature Spec followed: [yes / no / proposed]
|
||||
|
||||
## Assumptions & Plan
|
||||
|
||||
- Assumptions: ...
|
||||
- Plan:
|
||||
1. ...
|
||||
2. ...
|
||||
- Tradeoffs considered: ...
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Files changed: ...
|
||||
- Summary of modifications:
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Passes `make check`
|
||||
- [ ] Relevant tests pass (`make test-xxx`)
|
||||
- [ ] Complies with Constitution
|
||||
- [ ] Surgical & minimal (no unrelated changes)
|
||||
```
|
||||
|
||||
## 3. Behavioral Guidelines (Merged & Project-Specific)
|
||||
|
||||
**Think Before Coding**
|
||||
|
||||
- Don't assume. Don't hide confusion. Surface tradeoffs.
|
||||
- State assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them — don't pick silently.
|
||||
- If something is unclear (especially regarding wallets, extensions, or funding sources), stop and ask.
|
||||
|
||||
**Simplicity First**
|
||||
|
||||
- Minimum code that solves the problem. Nothing speculative.
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- Respect LNbits' lean core philosophy: new functionality should preferably go into an **extension** unless it truly belongs in core.
|
||||
|
||||
**Surgical Changes**
|
||||
|
||||
- Touch only what you must. Clean up only your own mess.
|
||||
- Match existing style (Python: Black + Ruff rules; JS: Prettier).
|
||||
- Do not "improve" or refactor adjacent code unless explicitly asked.
|
||||
- When editing, remove only imports/variables/functions made unused **by your changes**.
|
||||
- Never delete pre-existing dead code unless instructed.
|
||||
|
||||
**Goal-Driven Execution**
|
||||
|
||||
- Transform tasks into verifiable goals.
|
||||
- For tests: Write or update tests first when fixing bugs or adding behavior.
|
||||
- Always consider impact on existing extensions and multiple wallet backends (LND, CLN, Boltz, VoidWallet, etc.).
|
||||
|
||||
## 4. LNbits-Specific Rules
|
||||
|
||||
- **Extensions First**: Core should remain lean. Prefer implementing new features as extensions unless they are fundamental to wallets, security, or the API.
|
||||
- **Testing**: Use `FakeWallet` for unit/API tests. Regtest tests for full Lightning flows. Never break existing test targets in the Makefile.
|
||||
- **Dependencies**: Never add new dependencies without updating `pyproject.toml` and getting approval.
|
||||
- **Frontend**: JS/Vue code (e.g. `wallet.js`) must follow existing patterns and pass `make checkbundle` when static files are affected.
|
||||
- **Database / Migrations**: Do not make raw SQL changes. Use existing CRUD/services and migration tooling.
|
||||
- **Security**: Be extremely cautious with anything touching payments, keys, LNURL, Bolt11, or admin routes.
|
||||
- **Tools**: Use `uv run` for all commands. Prefer Makefile targets (`make format`, `make check`, `make test-xxx`).
|
||||
- **Generated Files**: Never modify gRPC files or other generated code.
|
||||
|
||||
## 5. Forbidden Behaviors
|
||||
|
||||
- Ignoring Constitution rules to "be helpful"
|
||||
- Large refactors without a spec or explicit request
|
||||
- Adding features "for future use"
|
||||
- Breaking backward compatibility for extensions or existing wallet backends
|
||||
- Committing code that fails `make check` or relevant tests
|
||||
- Exposing raw errors/stack traces to users
|
||||
- Using synchronous code in hot async paths without justification
|
||||
|
||||
## 6. How to Handle This File + Constitution
|
||||
|
||||
When the user gives you a task, start your response with:
|
||||
|
||||
> Following LNbits CONSTITUTION.md and AGENTS.md...
|
||||
|
||||
Then proceed with the structured format above.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:**
|
||||
|
||||
- Fewer unnecessary changes appear in diffs
|
||||
- Clarifying questions come **before** implementation
|
||||
- All changes respect the lean, extension-first, security-first nature of LNbits
|
||||
- Tests and `make check` continue to pass
|
||||
|
||||
Last Updated: April 2026
|
||||
@@ -0,0 +1,136 @@
|
||||
# CONSTITUTION.md
|
||||
|
||||
This is the immutable constitution of the LNbits project.
|
||||
Every feature spec, code change, refactor, extension, or decision by humans or AI agents **must comply** with this document.
|
||||
Changes to this file require explicit approval from the project owner/maintainers.
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
**Project Name:** LNbits
|
||||
**Core Purpose:** Free and open-source Lightning wallet and accounts system. A lightweight Python server that sits on top of any Lightning funding source, providing safe isolated wallets, a clean REST API, and a powerful extension system for adding features rapidly.
|
||||
**Target Users:** Individuals, communities, merchants, developers, and enterprises building on Bitcoin/Lightning (self-hosted or as part of larger stacks).
|
||||
**High-Level Success Criteria:**
|
||||
|
||||
- Reliable multi-wallet Lightning accounting with any backend (20+ supported funding sources)
|
||||
- Secure, extensible via 60+ extensions without bloating core
|
||||
- High code quality, test coverage, and backward compatibility for extensions
|
||||
- Production-ready performance and security for real Bitcoin value
|
||||
|
||||
**Version:** 1.5.4
|
||||
|
||||
## 2. Technology Stack (Strict)
|
||||
|
||||
- **Language:** Python >=3.10, <3.13 (strictly enforced via `pyproject.toml`)
|
||||
- **Framework:** FastAPI + Starlette (backend API)
|
||||
- **Frontend:** Vue.js + Quasar framework, with bundled static assets
|
||||
- **Database:** SQLite (aiosqlite) by default, PostgreSQL (asyncpg/psycopg2) supported via `LNBITS_DATABASE_URL`
|
||||
- **Async Runtime:** uvloop preferred
|
||||
- **Dependency Management:** uv + pyproject.toml (Hatchling build backend). Use `uv run` for all commands.
|
||||
- **Wallet Backends:** Abstracted via `lnbits.wallets` – support for LND, Core Lightning, Phoenixd, Boltz, Breez SDK, Liquid, VoidWallet fallback, etc. New backends must follow existing abstraction.
|
||||
- **Other Key Libs:** SQLAlchemy, Pydantic (v1), Loguru, Jinja2, LNURL, Bolt11, etc. (see `pyproject.toml` for pinned versions)
|
||||
- **Build/Frontend Tools:** npm for bundling (Quasar/Vue), Prettier for JS/CSS (check Makefile targets)
|
||||
|
||||
**Forbidden:**
|
||||
|
||||
- Adding new top-level dependencies without updating `pyproject.toml` **and** team approval
|
||||
- Using synchronous blocking calls in async paths (except where explicitly justified)
|
||||
- Direct database queries outside of CRUD layers or core services
|
||||
- Modifying generated files (e.g., gRPC files in wallets/boltz_grpc_files or lnd_grpc_files)
|
||||
|
||||
## 3. Architecture & Code Organization (Mandatory Rules)
|
||||
|
||||
- **Core Principle:** Modular monolith with heavy emphasis on **extensions**. All non-core features must live in extensions (installed via `lnbits/extensions`). Core stays lean.
|
||||
- **Backend Structure:**
|
||||
- `lnbits/core/` for core models, CRUD, services, routers, tasks
|
||||
- `lnbits/wallets/` for funding source abstractions
|
||||
- `lnbits/extensions/` for installed/upgradeable extensions (do not commit large extensions to core repo)
|
||||
- `lnbits/static/` for bundled frontend assets (managed via npm bundle)
|
||||
- **Key Rules:**
|
||||
- Use dependency injection and FastAPI routers properly
|
||||
- Extensions register routes/tasks via `register_ext_routes` / `register_ext_tasks`
|
||||
- Database migrations handled centrally (extension-specific migrations)
|
||||
- All new endpoints must be under proper versioning/prefixing where applicable
|
||||
- Frontend: Vue 2/Quasar components in `wallet.js` style (or updated) – keep reactive, use LNbits.utils helpers
|
||||
- No circular imports; respect existing middleware order (e.g., InstalledExtensionMiddleware before ExtensionsRedirectMiddleware)
|
||||
|
||||
**Exclusions** (do not lint/format these):
|
||||
|
||||
- `lnbits/extensions/`, `lnbits/upgrades/`, generated gRPC files, static/vendor bundles
|
||||
|
||||
## 4. Code Quality & Style
|
||||
|
||||
- **Formatting & Linting:**
|
||||
- Python: Black (line-length 88), Ruff (with selected rules: F, E, W, I, A, C, N, UP, RUF, B, S), MyPy (strict where possible), Pyright
|
||||
- JS/Frontend: Prettier
|
||||
- Run via Makefile: `make format` and `make check`
|
||||
- **Type Checking:** MyPy + Pyright enforced on `lnbits/`, `tests/`, `tools/`
|
||||
- **Testing Requirements:**
|
||||
- Unit, API, wallet, and regtest tests via pytest (see Makefile targets)
|
||||
- New core code or critical paths: high coverage expected (`--cov=lnbits`)
|
||||
- Extensions should include their own tests where possible
|
||||
- **Error Handling & Logging:** Use Loguru with structured context. Never expose raw stack traces to end users. Graceful fallbacks (e.g., VoidWallet on funding source failure).
|
||||
- **Pre-commit:** Strongly recommended (`make install-pre-commit-hook`)
|
||||
- **Bundle Integrity:** Frontend bundles must pass `make checkbundle` before commits affecting static files.
|
||||
|
||||
## 5. AI / LLM Usage Standards (if any agents or future AI features are added)
|
||||
|
||||
- Any new AI-powered features (e.g., via extensions) must use structured outputs (Pydantic/JSON mode)
|
||||
- Store prompts/templates versioned in the extension
|
||||
- Prefer deterministic behavior for financial/security paths (low temperature)
|
||||
- All AI outputs involving value/money must be validated server-side
|
||||
- Safety: Never allow untrusted model output to influence payments, wallet balances, or admin actions without guardrails
|
||||
|
||||
## 6. Safety, Security & Ethics
|
||||
|
||||
- **Critical:** Handle real Bitcoin/Lightning value → security-first mindset
|
||||
- Per-wallet isolation with separate admin/invoice/read keys
|
||||
- Rate limiting (SlowAPI) and IP blocking middleware mandatory
|
||||
- Sanitize all user inputs; validate LNURL, Bolt11, etc.
|
||||
- PII: Minimal collection; respect privacy (no unnecessary logging of sensitive data)
|
||||
- Funding source failures: Graceful degradation to VoidWallet + clear logging
|
||||
- Extensions: Hash-verified installs for vetted extensions; careful with custom extension paths
|
||||
- Audit logging via AuditMiddleware
|
||||
- Forbidden: Hard-coded secrets, insecure subprocess calls without review, SQL injection risks (use SQLAlchemy properly)
|
||||
|
||||
## 7. Performance & Cost Budgets
|
||||
|
||||
- Keep core lightweight – extensions handle heavy features
|
||||
- Async-first (uvloop, asyncpg/aiosqlite)
|
||||
- Reasonable retry logic for funding source connections (see `check_funding_source`)
|
||||
- Frontend: Optimized bundles (checkbundle enforced)
|
||||
- No unnecessary blocking operations in request paths
|
||||
|
||||
## 8. Development Workflow (Spec-Driven where possible)
|
||||
|
||||
- **All significant changes** should follow Spec-Driven Development:
|
||||
- Create/update Feature Spec in `.specify/` folder (or equivalent)
|
||||
- Reference this Constitution in every spec and PR
|
||||
- Use Makefile targets for format/check/test
|
||||
- Tests run with `FakeWallet` by default for unit/API; regtest for full flows
|
||||
- PRs must:
|
||||
- Pass `make check` and relevant tests
|
||||
- Include Constitution compliance notes (via AGENTS.md/CLAUDE.md)
|
||||
- Not break existing extensions or wallet backends
|
||||
- Branching: Protect main; use feature branches
|
||||
- Extensions: Develop separately; core repo focuses on framework stability
|
||||
|
||||
## 9. Decision Hierarchy (What Takes Precedence)
|
||||
|
||||
1. This Constitution
|
||||
2. Approved Feature Spec / Milestone
|
||||
3. Existing tests and backward compatibility (especially for extensions and wallet backends)
|
||||
4. Project maintainers / owner decision
|
||||
5. Everything else (including helpful AI suggestions)
|
||||
|
||||
If conflict: Stop, document the issue, and seek clarification from maintainers.
|
||||
|
||||
## 10. Amendment Process
|
||||
|
||||
- This Constitution can only be changed with explicit approval from project maintainers.
|
||||
- All changes must be dated, versioned, and reflected in `AGENTS.md`.
|
||||
- Minor clarifications can be proposed via PR with justification.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** April 2026 (based on v1.5.4)
|
||||
**Owner/Maintainers Approval:** LNbits Team
|
||||
@@ -1,30 +1,46 @@
|
||||
FROM python:3.10-slim-bullseye
|
||||
FROM python:3.12-slim-bookworm AS builder
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname automake
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Only copy the files required to install the dependencies
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN touch README.md
|
||||
|
||||
RUN mkdir data
|
||||
|
||||
RUN uv sync --all-extras
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
# needed for backups postgresql-client version 14 (pg_dump)
|
||||
RUN apt-get install -y apt-utils wget
|
||||
RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list
|
||||
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y postgresql-client-14
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get -y install gnupg2 curl lsb-release && \
|
||||
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
|
||||
curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
apt-get update && \
|
||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY --from=builder /app/.venv .venv
|
||||
|
||||
RUN mkdir data
|
||||
|
||||
RUN poetry install --only main
|
||||
RUN uv sync --all-extras
|
||||
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST"]
|
||||
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||
|
||||
@@ -6,77 +6,98 @@ format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier checkbundle
|
||||
|
||||
test: test-unit test-wallets test-api test-regtest
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write lnbits
|
||||
uv run ./node_modules/.bin/prettier --write .
|
||||
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
uv run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy
|
||||
uv run mypy
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
uv run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
uv run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
uv run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check lnbits
|
||||
uv run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
uv run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
dev:
|
||||
poetry run lnbits --reload
|
||||
uv run lnbits --reload
|
||||
|
||||
test:
|
||||
docker:
|
||||
docker build -t lnbits/lnbits .
|
||||
|
||||
test-wallets:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
FAKE_WALLET_SECRET="ToTheMoon1" \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
uv run pytest tests/wallets
|
||||
|
||||
test-real-wallet:
|
||||
test-unit:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
uv run pytest tests/unit
|
||||
|
||||
test-api:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
uv run pytest tests/api
|
||||
|
||||
test-regtest:
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
rm -rf ./tests/data \
|
||||
uv run pytest tests/regtest
|
||||
|
||||
test-migration:
|
||||
LNBITS_ADMIN_UI=True \
|
||||
make test
|
||||
make test-api
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5002 \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5002 \
|
||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
LNBITS_ADMIN_UI=False \
|
||||
timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
|
||||
poetry run python tools/conv.py
|
||||
uv run python tools/conv.py
|
||||
|
||||
migration:
|
||||
poetry run python tools/conv.py
|
||||
uv run python tools/conv.py
|
||||
|
||||
openapi:
|
||||
LNBITS_ADMIN_UI=False \
|
||||
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
|
||||
LNBITS_DATA_FOLDER="./tests/data" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=5003 \
|
||||
poetry run lnbits &
|
||||
uv run lnbits &
|
||||
sleep 15
|
||||
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
||||
curl -s http://0.0.0.0:5003/openapi.json | uv run openapi-spec-validator --errors=all -
|
||||
# kill -9 %1
|
||||
|
||||
bak:
|
||||
@@ -88,29 +109,26 @@ sass:
|
||||
|
||||
bundle:
|
||||
npm install
|
||||
npm run sass
|
||||
npm run vendor_copy
|
||||
npm run vendor_json
|
||||
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
||||
npm run vendor_bundle_css
|
||||
npm run vendor_minify_css
|
||||
npm run vendor_bundle_js
|
||||
npm run vendor_minify_js
|
||||
npm run bundle
|
||||
uv run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
|
||||
|
||||
checkbundle:
|
||||
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
|
||||
cp lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old
|
||||
cp lnbits/static/bundle-components.min.js lnbits/static/bundle-components.min.js.old
|
||||
make bundle
|
||||
diff -q lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old || exit 1
|
||||
diff -q lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old || exit 1
|
||||
diff -q lnbits/static/bundle-components.min.js lnbits/static/bundle-components.min.js.old || exit 1
|
||||
@echo "Bundle is OK"
|
||||
rm lnbits/static/bundle.min.js.old
|
||||
rm lnbits/static/bundle.min.css.old
|
||||
rm lnbits/static/bundle-components.min.js.old
|
||||
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||
poetry run pre-commit install
|
||||
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
||||
uv run pre-commit install
|
||||
|
||||
pre-commit:
|
||||
poetry run pre-commit run --all-files
|
||||
uv run pre-commit run --all-files
|
||||
|
||||
@@ -1,79 +1,88 @@
|
||||
# LNbits BETA
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
|
||||
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
[![license-badge]](LICENSE)
|
||||
[![docs-badge]][docs]
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||

|
||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||
|
||||
# Free and Open-Source Lightning Wallet Accounts System
|
||||
> Run it for yourself, for your community, or as part of a larger stack.
|
||||
|
||||
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
||||
## What is LNbits?
|
||||
|
||||
LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me
|
||||
LNbits is a lightweight Python server that sits on top of your Lightning funding source. It gives you safe, isolated wallets, a clean API, and an extension system for rapidly adding features - without locking you into a single node implementation. The Inspiration for LNBits came from ideas pioneered by **OpenNode** and **LNPay** — both today work as funding sources for LNbits.
|
||||
|
||||
Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits server!
|
||||
## What you can do with LNbits
|
||||
|
||||
LNbits is a Python server that sits on top of any funding source. It can be used as:
|
||||
- **Harden app security:** Create per-wallet API keys so individual apps never touch your full balance.
|
||||
- **Extend functionality fast:** Install extensions to explore and ship Lightning features with minimal code.
|
||||
- **Build into your stack:** Use the LNbits HTTP API to integrate payments, wallets, and accounting.
|
||||
- **Cover LNURL flows:** Use LNbits as a reliable fallback wallet for LNURL.
|
||||
- **Demo in minutes:** Spin up instant wallets for workshops, proofs-of-concept, and user testing.
|
||||
|
||||
- Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
|
||||
- Extendable platform for exploring Lightning network functionality via the LNbits extension framework
|
||||
- Part of a development stack via LNbits API
|
||||
- Fallback wallet for the LNURL scheme
|
||||
- Instant wallet for LN demonstrations
|
||||
## Funding sources
|
||||
|
||||
LNbits can run on top of any Lightning funding source. It currently supports the following, but more and more are added regularly:
|
||||
- LND (REST and gRPC)
|
||||
- Core Lightning aka CLN (gRPC, REST and Spark)
|
||||
- Eclair
|
||||
- LNPay
|
||||
- LNbits
|
||||
- OpenNode
|
||||
- Alby
|
||||
- ZBD
|
||||
- LightningTipBot
|
||||
LNbits runs on top of most Lightning backends. Choose the one you already operate - or swap later without changing your app architecture.
|
||||
|
||||
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
|
||||
- Read the [funding source guide](https://docs.lnbits.org/guide/wallets.html)
|
||||
|
||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||
## Learn more
|
||||
|
||||
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as funding sources for LNbits.
|
||||
- Video series on [Youtube](https://www.youtube.com/@lnbits)
|
||||
- Introduction Video [LNBits V1](https://www.youtube.com/watch?v=PFAHKxvgI9Y&t=19s)
|
||||
|
||||
## Running LNbits
|
||||
|
||||
See the [install guide](https://github.com/lnbits/lnbits/blob/main/docs/guide/installation.md) for details on installation and setup.
|
||||
|
||||
## LNbits as an account system
|
||||
Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo.lnbits.com), or on [lnbits.com](https://lnbits.com) software as a service, where you can spin up an LNbits instance for 21sats per hr.
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
|
||||
## LNbits account system
|
||||
|
||||

|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
|
||||
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
|
||||
|
||||
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
|
||||
## LNbits extension universe
|
||||
|
||||

|
||||
Extend YOUR LNbits to meet YOUR needs.
|
||||
|
||||
## LNbits as an LNURL-withdraw fallback
|
||||
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
||||
|
||||
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the [LNURL-withdraw](https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-withdraw.md).
|
||||
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
|
||||
|
||||

|
||||
## LNbits API
|
||||
|
||||
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
|
||||
Example use would be an ATM, which utilizes LNURL, if the user scans the QR with a regular QR code scanner app, they will still be able to access the funds.
|
||||
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
||||
|
||||

|
||||
<img alt="lnbits_api" src="docs/assets/api.jpg" />
|
||||
|
||||
## LNbits as an instant wallet
|
||||
## LNbits node manager
|
||||
|
||||
Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app".
|
||||
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
||||
|
||||

|
||||
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
|
||||
|
||||
## Tip us
|
||||
## LNbits merchant tools
|
||||
|
||||
If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
|
||||
|
||||
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
ARG LNBITS_TAG=latest
|
||||
|
||||
FROM boltz/boltz-client:latest AS boltz
|
||||
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||
|
||||
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
|
||||
RUN ls -l /usr/local/bin/boltzd
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y netcat-openbsd
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Reinstall dependencies just in case (needed for CMD usage)
|
||||
RUN uv sync --all-extras
|
||||
|
||||
# LNbits
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES="10"
|
||||
|
||||
# Boltzd
|
||||
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
||||
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
|
||||
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
|
||||
ENV BOLTZ_CLIENT_WALLET="lnbits"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
boltzd --standalone --referralId lnbits &
|
||||
|
||||
# Capture boltzd PID to monitor if needed
|
||||
BOLTZ_PID=$!
|
||||
|
||||
# Wait for boltzd to start
|
||||
for i in {1..10}; do
|
||||
if nc -z localhost 9002; then
|
||||
echo "boltzd is up!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for boltzd to start..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Optional: check if still not up
|
||||
if ! nc -z localhost 9002; then
|
||||
echo "boltzd did not start successfully."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||
@@ -0,0 +1,18 @@
|
||||
!data
|
||||
data/*
|
||||
|
||||
!data/boltz
|
||||
data/boltz/*
|
||||
!data/boltz/boltz.conf
|
||||
|
||||
!data/boltz-client
|
||||
data/boltz-client/*
|
||||
!data/boltz-client/boltz.toml
|
||||
|
||||
!data/boltz-nginx
|
||||
data/boltz-nginx/*
|
||||
!data/boltz-nginx/default.conf
|
||||
|
||||
!data/eclair
|
||||
data/eclair/*
|
||||
!data/eclair/eclair.conf
|
||||
@@ -0,0 +1,99 @@
|
||||

|
||||
|
||||
# nodes
|
||||
|
||||
- lnd-1: for locally testing your current lnbits
|
||||
- lnd-2: used for boltz backend
|
||||
- lnd-3: used for lnbits inside docker
|
||||
- cln-1: for locally testing your current lnbits
|
||||
- cln-2: used for clightning-REST
|
||||
- eclair-1: for locally testing your current lnbits
|
||||
|
||||
# Installing regtest
|
||||
|
||||
get the regtest enviroment ready
|
||||
|
||||
```sh
|
||||
# Install docker https://docs.docker.com/engine/install/
|
||||
# Make sure your user has permission to use docker 'sudo usermod -aG docker ${USER}' then reboot
|
||||
# Stop/start docker 'sudo systemctl stop docker' 'sudo systemctl start docker'
|
||||
|
||||
sudo apt install jq
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits/lnbits .
|
||||
mkdir docker
|
||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||
cd docker
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest # start the regtest and also run tests
|
||||
sudo chown -R $USER ./data # Give the data file permissions for user
|
||||
```
|
||||
|
||||
# Running LNbits on regtest
|
||||
|
||||
add this ENV variables to your `.env` file
|
||||
|
||||
```sh
|
||||
DEBUG=true
|
||||
|
||||
# LND
|
||||
LNBITS_BACKEND_WALLET_CLASS="LndRestWallet"
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8081/
|
||||
LND_REST_CERT=/home/user/repos/lnbits/docker/data/lnd-1/tls.cert
|
||||
LND_REST_MACAROON=/home/user/repos/lnbits/docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
|
||||
# CLN
|
||||
LNBITS_BACKEND_WALLET_CLASS="CoreLightningWallet"
|
||||
CORELIGHTNING_RPC=./docker/data/clightning-1/regtest/lightning-rpc
|
||||
|
||||
|
||||
# Run LNbits
|
||||
uv run lnbits
|
||||
|
||||
# Run LNbits with hot reload
|
||||
make dev
|
||||
```
|
||||
|
||||
# testing
|
||||
|
||||
```sh
|
||||
chmod +x ./start-regtest
|
||||
./start-regtest
|
||||
# short answer :)
|
||||
./start-regtest && echo "PASSED" || echo "FAILED" > /dev/null
|
||||
```
|
||||
|
||||
usage of the `bitcoin-cli-sim`, `lightning-cli-sim` and `lncli-sim` aliases
|
||||
|
||||
```sh
|
||||
cd ~/lnbits/docker
|
||||
source docker-scripts.sh
|
||||
# use bitcoin core, mine a block
|
||||
bitcoin-cli-sim -generate 1
|
||||
|
||||
# use c-lightning nodes
|
||||
lightning-cli-sim 1 newaddr | jq -r '.bech32' # use node 1
|
||||
lightning-cli-sim 2 getinfo # use node 2
|
||||
lightning-cli-sim 3 getinfo # use node 3
|
||||
|
||||
# use lnd nodes
|
||||
lncli-sim 1 newaddr p2wsh
|
||||
lncli-sim 2 listpeers
|
||||
```
|
||||
|
||||
# urls
|
||||
|
||||
- mempool: http://localhost:8080/
|
||||
- boltz api: http://localhost:9001/
|
||||
- lnd-1 rest: http://localhost:8081/
|
||||
- lnbits: http://localhost:5001/
|
||||
|
||||
# debugging docker logs
|
||||
|
||||
```sh
|
||||
docker logs lnbits-lnbits-1 -f
|
||||
docker logs lnbits-boltz-1 -f
|
||||
docker logs lnbits-clightning-1-1 -f
|
||||
docker logs lnbits-lnd-2-1 -f
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
standalone = true
|
||||
network = "regtest"
|
||||
|
||||
# Path the the log file
|
||||
logfile = ""
|
||||
|
||||
electrumUrl = "electrs:19001"
|
||||
electrumLiquidUrl = "electrs-liquid:19002"
|
||||
|
||||
[BOLTZ]
|
||||
# By default the daemon automatically connects to the official Boltz instance for the network LND is on
|
||||
# This value is used to override that
|
||||
url = "http://boltz-nginx:9001"
|
||||
|
||||
[DATABASE]
|
||||
# Path to the SQLite database file
|
||||
# path = "/home/michael/test.db"
|
||||
|
||||
[RPC]
|
||||
# Host of
|
||||
host = "0.0.0.0"
|
||||
|
||||
# Port of the gRPC interface
|
||||
port = 9002
|
||||
|
||||
# Whether the REST proxy for the gRPC interface should be disabled
|
||||
restDisabled = false
|
||||
|
||||
# Host of the REST proxy
|
||||
restHost = "0.0.0.0"
|
||||
|
||||
# Port of the REST proxy
|
||||
restPort = 9003
|
||||
|
||||
# Path to the TLS cert for the gRPC and REST interface
|
||||
tlsCert = ""
|
||||
|
||||
# Path to the TLS private key for the gRPC and REST interface
|
||||
tlsKey = ""
|
||||
noTls = true
|
||||
|
||||
# Whether the macaroon authentication for the gRPC and REST interface should be disabled
|
||||
noMacaroons = true
|
||||
|
||||
# Path to the admin macaroon for the gRPC and REST interface
|
||||
adminMacaroonPath = ""
|
||||
|
||||
# Path to the read only macaroon for the gRPC and REST interface
|
||||
readOnlyMacaroonPath = ""
|
||||
@@ -0,0 +1,49 @@
|
||||
upstream boltz {
|
||||
server boltz:9001;
|
||||
}
|
||||
|
||||
upstream boltzr {
|
||||
server boltz:9005;
|
||||
}
|
||||
|
||||
upstream ws {
|
||||
server boltz:9004;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9001;
|
||||
listen [::]:9001;
|
||||
server_name localhost;
|
||||
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods 'GET, PATCH, DELETE, POST, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers "*" always;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
location /v2/ws {
|
||||
proxy_pass http://ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
|
||||
location ~ ^/v2/(lightning|swap/rescue|swap/restore) {
|
||||
proxy_pass http://boltzr;
|
||||
}
|
||||
|
||||
location /streamswapstatus {
|
||||
proxy_pass http://boltzr;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://boltz;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
[api]
|
||||
host = "0.0.0.0"
|
||||
port = 9_001
|
||||
|
||||
[grpc]
|
||||
host = "0.0.0.0"
|
||||
port = 9_000
|
||||
|
||||
[postgres]
|
||||
host = "boltz-postgres"
|
||||
port = 5432
|
||||
database = "boltz"
|
||||
username = "boltz"
|
||||
password = "boltz"
|
||||
|
||||
[sidecar]
|
||||
[sidecar.grpc]
|
||||
host = "127.0.0.1"
|
||||
port = 9003
|
||||
|
||||
[sidecar.ws]
|
||||
host = "0.0.0.0"
|
||||
port = 9004
|
||||
|
||||
[sidecar.api]
|
||||
host = "0.0.0.0"
|
||||
port = 9005
|
||||
|
||||
[swap]
|
||||
deferredClaimSymbols = ["BTC", "L-BTC"]
|
||||
|
||||
[[pairs]]
|
||||
base = "BTC"
|
||||
quote = "BTC"
|
||||
rate = 1
|
||||
fee = 0.5
|
||||
swapInFee = 0.1
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 50_000
|
||||
|
||||
[pairs.timeoutDelta]
|
||||
chain = 1440
|
||||
reverse = 1440
|
||||
swapMinimal = 1440
|
||||
swapMaximal = 2880
|
||||
swapTaproot = 10080
|
||||
|
||||
[[pairs]]
|
||||
base = "L-BTC"
|
||||
quote = "BTC"
|
||||
fee = 0.25
|
||||
swapInFee = 0.1
|
||||
rate = 1
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 100
|
||||
|
||||
[pairs.submarineSwap]
|
||||
minSwapAmount = 1_000
|
||||
minBatchedAmount = 21
|
||||
|
||||
[pairs.chainSwap]
|
||||
minSwapAmount = 25_000
|
||||
|
||||
[pairs.timeoutDelta]
|
||||
chain = 1440
|
||||
reverse = 1440
|
||||
swapMinimal = 1440
|
||||
swapMaximal = 2880
|
||||
swapTaproot = 10080
|
||||
|
||||
[[currencies]]
|
||||
symbol = "BTC"
|
||||
network = "bitcoinRegtest"
|
||||
minWalletBalance = 10_000_000
|
||||
minChannelBalance = 10_000_000
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 0
|
||||
|
||||
[currencies.chain]
|
||||
# mempoolSpace = "http://mempool-web:8090/api"
|
||||
host = "bitcoind"
|
||||
zmqpubrawtx = "tcp://bitcoind:29000"
|
||||
zmqpubrawblock = "tcp://bitcoind:29001"
|
||||
port = 18_443
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
feeFloor = 0.2
|
||||
|
||||
wallet = "lnbits"
|
||||
|
||||
[currencies.lnd]
|
||||
host = "lnd-2"
|
||||
port = 10_009
|
||||
certpath = "/data/lnd/tls.cert"
|
||||
macaroonpath = "/data/lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||
|
||||
|
||||
[liquid]
|
||||
symbol = "L-BTC"
|
||||
network = "liquidRegtest"
|
||||
|
||||
maxSwapAmount = 40_294_967
|
||||
minSwapAmount = 10_000
|
||||
maxZeroConfAmount = 40_294_967
|
||||
|
||||
|
||||
[liquid.chain]
|
||||
host = "elementsd"
|
||||
port = 18884
|
||||
cookie = "/root/.elements/liquidregtest/.cookie"
|
||||
zmqpubrawtx = "tcp://elementsd:31000"
|
||||
zmqpubhashblock = "tcp://elementsd:31002"
|
||||
|
||||
wallet = "lnbits"
|
||||
@@ -0,0 +1,28 @@
|
||||
eclair {
|
||||
chain = "regtest"
|
||||
|
||||
api {
|
||||
binding-ip = "0.0.0.0"
|
||||
enabled = true
|
||||
port = 8080
|
||||
password = "lnbits"
|
||||
}
|
||||
|
||||
bitcoind {
|
||||
host = "bitcoind"
|
||||
rpcport = 18443
|
||||
auth = "safecookie"
|
||||
cookie = "/root/.bitcoin/regtest/.cookie"
|
||||
|
||||
zmqblock = "tcp://bitcoind:29002"
|
||||
zmqtx = "tcp://bitcoind:29000"
|
||||
}
|
||||
|
||||
channel {
|
||||
max-funding-satoshis = 10000000000
|
||||
}
|
||||
|
||||
features {
|
||||
option_support_large_channel = mandatory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
services:
|
||||
|
||||
lnbits:
|
||||
hostname: lnbits
|
||||
depends_on:
|
||||
- lnd-3
|
||||
image: lnbits/lnbits
|
||||
restart: on-failure
|
||||
user: "0:0"
|
||||
environment:
|
||||
LNBITS_PORT: 5001
|
||||
DEBUG: true
|
||||
LNBITS_ADMIN_UI: false
|
||||
LNBITS_BACKEND_WALLET_CLASS: "LndRestWallet"
|
||||
LNBITS_DATA_FOLDER: "./data"
|
||||
LND_REST_ENDPOINT: "https://lnd-3:8081/"
|
||||
LND_REST_CERT: "./lnd/tls.cert"
|
||||
LND_REST_MACAROON: "./lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||
ports:
|
||||
- 5001:5001
|
||||
volumes:
|
||||
- lnbits-data:/app/data
|
||||
- ./data/lnd-3:/app/lnd:uid=1000,gid=1000
|
||||
|
||||
boltz:
|
||||
hostname: boltz
|
||||
depends_on:
|
||||
- lnd-2
|
||||
- boltz-postgres
|
||||
restart: always
|
||||
image: boltz/boltz:v3.12.1
|
||||
ports:
|
||||
- 9000:9000
|
||||
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
|
||||
volumes:
|
||||
- ./data/lnd-2:/data/lnd/
|
||||
- ./data/boltz/:/root/.boltz/
|
||||
- elements-data:/root/.elements
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
boltz-client:
|
||||
hostname: boltz-client
|
||||
depends_on:
|
||||
- boltz
|
||||
restart: always
|
||||
image: boltz/boltz-client:latest
|
||||
ports:
|
||||
- 9002:9002
|
||||
- 9003:9003
|
||||
expose:
|
||||
- 9002
|
||||
healthcheck:
|
||||
test: ['CMD', 'boltzcli', '--host', 'boltz-client', 'getinfo']
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 0s
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
- ./data/boltz-client:/root/.boltz
|
||||
|
||||
boltz-backend-nginx:
|
||||
hostname: boltz-nginx
|
||||
restart: always
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- 9001:9001
|
||||
volumes:
|
||||
- nginx-data:/etc/nginx/conf.d
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'curl http://localhost:9001/version']
|
||||
timeout: 1s
|
||||
retries: 10
|
||||
interval: 1s
|
||||
start_period: 0s
|
||||
|
||||
boltz-postgres:
|
||||
hostname: boltz-postgres
|
||||
restart: always
|
||||
image: postgres:14-alpine
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready --dbname boltz --username boltz"]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
environment:
|
||||
- POSTGRES_DB=boltz
|
||||
- POSTGRES_USER=boltz
|
||||
- POSTGRES_PASSWORD=boltz
|
||||
expose:
|
||||
- 5432
|
||||
|
||||
bitcoind:
|
||||
hostname: bitcoind
|
||||
image: boltz/bitcoin-core:25.0
|
||||
command:
|
||||
- -regtest
|
||||
- -fallbackfee=0.00000253
|
||||
- -zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
- -zmqpubrawblock=tcp://0.0.0.0:29001
|
||||
- -zmqpubhashblock=tcp://0.0.0.0:29002
|
||||
- -txindex
|
||||
- -rpcallowip=0.0.0.0/0
|
||||
- -rpcbind=0.0.0.0
|
||||
- -addresstype=bech32
|
||||
- -changetype=bech32
|
||||
- -dbcache=2048
|
||||
- -rpcworkqueue=256
|
||||
volumes:
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
expose:
|
||||
- 29000
|
||||
- 29001
|
||||
- 29002
|
||||
- 18443
|
||||
- 18444
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"bitcoin-cli",
|
||||
"--rpccookiefile=/root/.bitcoin/regtest/.cookie",
|
||||
"-regtest",
|
||||
"getblockchaininfo",
|
||||
]
|
||||
timeout: 1s
|
||||
retries: 1
|
||||
interval: 1s
|
||||
start_period: 0s
|
||||
|
||||
clightning-1:
|
||||
hostname: clightning-1
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:24.11
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9736
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
- --clnrest-host=0.0.0.0
|
||||
- --clnrest-port=3010
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9736:9736
|
||||
- 3010:3010
|
||||
volumes:
|
||||
- ./data/clightning-1:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-2:
|
||||
hostname: clightning-2
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:22.11.1
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9737
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9737:9737
|
||||
volumes:
|
||||
- ./data/clightning-2:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-2-rest:
|
||||
hostname: clightning-2-rest
|
||||
depends_on:
|
||||
- clightning-2
|
||||
image: saubyk/c-lightning-rest:0.10.7
|
||||
entrypoint: "sh -c 'sleep 35 && /sbin/tini -g -- ./docker-entrypoint.sh'"
|
||||
ports:
|
||||
- 3001:3001
|
||||
expose:
|
||||
- 3001
|
||||
volumes:
|
||||
- ./data/clightning-2:/root/.lightning/:uid=1000,gid=1000
|
||||
- ./data/clightning-2-rest:/usr/src/app/certs/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
clightning-3:
|
||||
hostname: clightning-3
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/c-lightning:24.11
|
||||
command:
|
||||
- --large-channels
|
||||
- --network=regtest
|
||||
- --grpc-port=9738
|
||||
- --bind-addr=0.0.0.0:9735
|
||||
- --bitcoin-rpcconnect=bitcoind
|
||||
- --bitcoin-rpcport=18443
|
||||
expose:
|
||||
- 9735
|
||||
ports:
|
||||
- 9738:9738
|
||||
volumes:
|
||||
- ./data/clightning-3:/root/.lightning/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-1:
|
||||
hostname: lnd-1
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-1:9735
|
||||
- --rpclisten=lnd-1:10009
|
||||
- --restlisten=lnd-1:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-1:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-2:
|
||||
hostname: lnd-2
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.19.3-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-2:9735
|
||||
- --rpclisten=lnd-2:10009
|
||||
- --restlisten=lnd-2:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-2:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-3:
|
||||
hostname: lnd-3
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-3:9735
|
||||
- --rpclisten=lnd-3:10009
|
||||
- --restlisten=lnd-3:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
ports:
|
||||
- 8081:8081
|
||||
- 10009:10009
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-3:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
lnd-4:
|
||||
hostname: lnd-4
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/lnd:0.18.4-beta
|
||||
restart: on-failure
|
||||
command:
|
||||
- --listen=lnd-4:9735
|
||||
- --rpclisten=lnd-4:10009
|
||||
- --restlisten=lnd-4:8081
|
||||
- --bitcoin.active
|
||||
- --bitcoin.regtest
|
||||
- --bitcoin.node=bitcoind
|
||||
- --bitcoind.rpchost=bitcoind
|
||||
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
|
||||
- --bitcoind.zmqpubrawtx=bitcoind:29000
|
||||
- --bitcoind.zmqpubrawblock=bitcoind:29001
|
||||
- --noseedbackup
|
||||
- --protocol.wumbo-channels
|
||||
expose:
|
||||
- 8081
|
||||
- 9735
|
||||
- 10009
|
||||
volumes:
|
||||
- ./data/lnd-4:/root/.lnd/
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
eclair:
|
||||
hostname: eclair
|
||||
depends_on:
|
||||
- bitcoind
|
||||
image: boltz/eclair:0.8.0
|
||||
restart: on-failure
|
||||
entrypoint: "sh -c 'JAVA_OPTS=-Xmx512m /eclair-node/bin/eclair-node.sh -Declair.datadir=/root/eclair -Declair.printToConsole'"
|
||||
ports:
|
||||
- 8082:8080
|
||||
expose:
|
||||
- 9735
|
||||
- 8080
|
||||
volumes:
|
||||
- ./data/eclair:/root/eclair
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
electrs:
|
||||
hostname: electrs
|
||||
restart: always
|
||||
image: boltz/electrs:latest
|
||||
entrypoint: ["electrs-bitcoin"]
|
||||
command:
|
||||
- --electrum-rpc-addr
|
||||
- electrs:19001
|
||||
- --http-addr
|
||||
- electrs:3002
|
||||
- --daemon-rpc-addr
|
||||
- bitcoind:18443
|
||||
- --network
|
||||
- regtest
|
||||
- --jsonrpc-import
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s $(hostname):3002/blocks/tip/height"]
|
||||
timeout: 1s
|
||||
retries: 20
|
||||
interval: 2s
|
||||
start_period: 5s
|
||||
ports:
|
||||
- 19001:19001
|
||||
- 3002:3002
|
||||
volumes:
|
||||
- bitcoin-data:/root/.bitcoin
|
||||
|
||||
elementsd:
|
||||
hostname: elementsd
|
||||
restart: always
|
||||
image: boltz/elements:latest
|
||||
expose:
|
||||
- 31001
|
||||
ports:
|
||||
- 31000:31000
|
||||
- 31002:31002
|
||||
- 18884:18884
|
||||
command:
|
||||
- -chain=liquidregtest
|
||||
- -txindex=1
|
||||
- -rest=1
|
||||
- -server=1
|
||||
- -rpcallowip=0.0.0.0/0
|
||||
- -validatepegin=0
|
||||
- -initialfreecoins=2100000000000000
|
||||
- -fallbackfee=0.000001
|
||||
- -rpcbind=0.0.0.0
|
||||
- -rpcport=18884
|
||||
- -zmqpubrawtx=tcp://0.0.0.0:31000
|
||||
- -zmqpubrawblock=tcp://0.0.0.0:31001
|
||||
- -zmqpubhashblock=tcp://0.0.0.0:31002
|
||||
- -acceptdiscountct=1
|
||||
- -creatediscountct=1
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
|
||||
electrs-liquid:
|
||||
hostname: electrs-liquid
|
||||
restart: always
|
||||
image: boltz/electrs:latest
|
||||
entrypoint: ["electrs-liquid"]
|
||||
command:
|
||||
- --electrum-rpc-addr
|
||||
- electrs-liquid:19002
|
||||
- --http-addr
|
||||
- electrs-liquid:3003
|
||||
- --daemon-rpc-addr
|
||||
- elementsd:18884
|
||||
- --daemon-dir
|
||||
- /root/.elements
|
||||
- --network
|
||||
- liquidregtest
|
||||
- --parent-network
|
||||
- regtest
|
||||
- --jsonrpc-import
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s $(hostname):3003/blocks/tip/height"]
|
||||
timeout: 15s
|
||||
retries: 20
|
||||
interval: 2s
|
||||
start_period: 5s
|
||||
ports:
|
||||
- 19002:19002
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- elements-data:/root/.elements
|
||||
|
||||
volumes:
|
||||
lnbits-data:
|
||||
bitcoin-data:
|
||||
elements-data:
|
||||
nginx-data:
|
||||
name: nginx-data
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ./data/boltz-nginx/
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/bin/sh
|
||||
export COMPOSE_PROJECT_NAME=lnbits
|
||||
|
||||
boltzcli-sim() {
|
||||
docker exec -it lnbits-boltz-client-1 boltzcli "$@"
|
||||
}
|
||||
|
||||
bitcoin-cli-sim() {
|
||||
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest "$@"
|
||||
}
|
||||
|
||||
elements-cli-sim() {
|
||||
docker exec lnbits-elementsd-1 elements-cli -rpcport=18884 -chain=liquidregtest "$@"
|
||||
}
|
||||
|
||||
# args(i, cmd)
|
||||
lightning-cli-sim() {
|
||||
i=$1
|
||||
shift # shift first argument so we can use $@
|
||||
docker exec lnbits-clightning-$i-1 lightning-cli --network regtest "$@"
|
||||
}
|
||||
|
||||
# args(i, cmd)
|
||||
lncli-sim() {
|
||||
i=$1
|
||||
shift # shift first argument so we can use $@
|
||||
docker exec lnbits-lnd-$i-1 lncli --network regtest --rpcserver=lnd-$i:10009 "$@"
|
||||
}
|
||||
|
||||
get-eclair-pubkey() {
|
||||
while true; do
|
||||
pubkey=$(docker exec lnbits-eclair-1 curl http://localhost:8080/getinfo -X POST -s -u :lnbits | jq -r .nodeId 2> /dev/null)
|
||||
pubkeyPrefix=$(echo $pubkey | cut -c1,2)
|
||||
if [[ "$pubkeyPrefix" == "02" || "$pubkeyPrefix" == "03" ]]; then
|
||||
echo $pubkey
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-eclair-channel() {
|
||||
while true; do
|
||||
state=$(docker exec lnbits-eclair-1 curl http://localhost:8080/channels -X POST -s -u :lnbits | jq -r ".[0].state")
|
||||
pending=$(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
|
||||
echo "eclair-1 pendingchannels: $pending, current state: $state"
|
||||
if [[ "$state" == "NORMAL" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# args(i)
|
||||
fund_boltz_client() {
|
||||
# first address of seed: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
||||
address="el1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f3mmz5l7uw5pqmx6xf5xy50hsn6vhkm5euwt72x878eq6zxx2z0z676mna6kdq"
|
||||
echo "funding: $address on boltz-client"
|
||||
elements-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
|
||||
# args(i)
|
||||
fund_clightning_node() {
|
||||
address=$(lightning-cli-sim $1 newaddr | jq -r .bech32)
|
||||
echo "funding: $address on clightning-node: $1"
|
||||
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
# args(i)
|
||||
fund_lnd_node() {
|
||||
address=$(lncli-sim $1 newaddress p2wkh | jq -r .address)
|
||||
echo "funding: $address on lnd-node: $1"
|
||||
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
|
||||
}
|
||||
|
||||
# args(i, j)
|
||||
connect_clightning_node() {
|
||||
pubkey=$(lightning-cli-sim $2 getinfo | jq -r '.id')
|
||||
lightning-cli-sim $1 connect $pubkey@lnbits-clightning-$2-1:9735 | jq -r '.id'
|
||||
}
|
||||
|
||||
lnbits-regtest-start(){
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed"
|
||||
exit
|
||||
fi
|
||||
if ! command -v docker &> /dev/null
|
||||
then
|
||||
echo "docker is not installed"
|
||||
exit
|
||||
fi
|
||||
if ! command -v docker version &> /dev/null
|
||||
then
|
||||
echo "dockerd is not running"
|
||||
exit
|
||||
fi
|
||||
lnbits-regtest-stop
|
||||
docker compose up -d --remove-orphans
|
||||
lnbits-regtest-init
|
||||
}
|
||||
|
||||
lnbits-regtest-start-log(){
|
||||
lnbits-regtest-stop
|
||||
docker compose up --remove-orphans
|
||||
lnbits-regtest-init
|
||||
}
|
||||
|
||||
lnbits-regtest-stop(){
|
||||
docker compose down --volumes
|
||||
# clean up lightning node data
|
||||
sudo rm -rf ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4 ./data/boltz/boltz.db ./data/eclair/regtest ./data/boltz-client/liquid-wallet ./data/boltz-client/bitcoin-wallet ./data/boltz-client/wallet ./data/boltz-client/boltz.db
|
||||
# recreate lightning node data folders preventing permission errors
|
||||
mkdir ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4
|
||||
}
|
||||
|
||||
lnbits-regtest-restart(){
|
||||
lnbits-regtest-stop
|
||||
lnbits-regtest-start
|
||||
}
|
||||
|
||||
boltz-client-init(){
|
||||
for i in 0 1 2; do
|
||||
fund_boltz_client
|
||||
done
|
||||
elements-cli-sim -generate 3 > /dev/null
|
||||
}
|
||||
|
||||
lnbits-bitcoin-init(){
|
||||
echo "init_bitcoin_wallet..."
|
||||
bitcoin-cli-sim createwallet lnbits || bitcoin-cli-sim loadwallet lnbits
|
||||
echo "mining 150 blocks..."
|
||||
bitcoin-cli-sim -generate 150 > /dev/null
|
||||
}
|
||||
|
||||
lnbits-elements-init(){
|
||||
echo "init_elements_wallet..."
|
||||
elements-cli-sim createwallet lnbits || elements-cli-sim loadwallet lnbits
|
||||
echo "mining 150 blocks..."
|
||||
elements-cli-sim -generate 150 > /dev/null
|
||||
elements-cli-sim rescanblockchain
|
||||
}
|
||||
|
||||
lnbits-init(){
|
||||
echo "init_lnbits..."
|
||||
docker exec lnbits-lnbits-1 uv run python tools/create_fake_admin.py
|
||||
}
|
||||
|
||||
lnbits-regtest-init(){
|
||||
lnbits-bitcoin-init
|
||||
lnbits-elements-init
|
||||
lnbits-lightning-sync
|
||||
lnbits-lightning-init
|
||||
boltz-client-init
|
||||
lnbits-init
|
||||
}
|
||||
|
||||
lnbits-lightning-sync(){
|
||||
wait-for-clightning-sync 1
|
||||
wait-for-clightning-sync 2
|
||||
wait-for-clightning-sync 3
|
||||
wait-for-lnd-sync 1
|
||||
wait-for-lnd-sync 2
|
||||
wait-for-lnd-sync 3
|
||||
wait-for-lnd-sync 4
|
||||
}
|
||||
|
||||
lnbits-lightning-init(){
|
||||
|
||||
# create 10 UTXOs for each node
|
||||
for i in 0 1 2; do
|
||||
fund_clightning_node 1
|
||||
fund_clightning_node 2
|
||||
fund_clightning_node 3
|
||||
fund_lnd_node 1
|
||||
fund_lnd_node 2
|
||||
fund_lnd_node 3
|
||||
fund_lnd_node 4
|
||||
done
|
||||
|
||||
echo "mining 3 blocks..."
|
||||
bitcoin-cli-sim -generate 3 > /dev/null
|
||||
|
||||
lnbits-lightning-sync
|
||||
|
||||
channel_confirms=6
|
||||
channel_size=24000000 # 0.024 btc
|
||||
balance_size=12000000 # 0.12 btc
|
||||
balance_size_msat=12000000000 # 0.12 btc
|
||||
|
||||
# lnd-1 -> lnd-2
|
||||
lncli-sim 1 connect $(lncli-sim 2 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-2-1 > /dev/null
|
||||
echo "open channel from lnd-1 to lnd-2"
|
||||
lncli-sim 1 openchannel $(lncli-sim 2 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> lnd-3
|
||||
lncli-sim 1 connect $(lncli-sim 3 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-3-1 > /dev/null
|
||||
echo "open channel from lnd-1 to lnd-3"
|
||||
lncli-sim 1 openchannel $(lncli-sim 3 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-1
|
||||
lncli-sim 1 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-1"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-2
|
||||
lncli-sim 1 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-2"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-1 -> cln-3
|
||||
lncli-sim 1 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
|
||||
echo "open channel from lnd-1 to cln-3"
|
||||
lncli-sim 1 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-2 -> cln-2
|
||||
lncli-sim 2 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
|
||||
echo "open channel from lnd-2 to cln-2"
|
||||
lncli-sim 2 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 2
|
||||
|
||||
# lnd-3 -> cln-3
|
||||
lncli-sim 3 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
|
||||
echo "open channel from lnd-3 to cln-1"
|
||||
lncli-sim 3 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 3
|
||||
|
||||
# lnd-3 -> cln-1
|
||||
lncli-sim 3 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
|
||||
echo "open channel from lnd-3 to cln-1"
|
||||
lncli-sim 3 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 3
|
||||
|
||||
# lnd-1 -> eclair-1
|
||||
lncli-sim 1 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
|
||||
echo "open channel from lnd-2 to eclair-1"
|
||||
lncli-sim 1 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 1
|
||||
|
||||
# lnd-2 -> eclair-1
|
||||
lncli-sim 2 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
|
||||
echo "open channel from lnd-2 to eclair-1"
|
||||
lncli-sim 2 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
|
||||
bitcoin-cli-sim -generate $channel_confirms > /dev/null
|
||||
wait-for-lnd-channel 2
|
||||
|
||||
wait-for-clightning-channel 1
|
||||
wait-for-clightning-channel 2
|
||||
wait-for-clightning-channel 3
|
||||
|
||||
wait-for-eclair-channel
|
||||
|
||||
lnbits-lightning-sync
|
||||
|
||||
}
|
||||
|
||||
wait-for-lnd-channel(){
|
||||
while true; do
|
||||
pending=$(lncli-sim $1 pendingchannels | jq -r '.pending_open_channels | length')
|
||||
echo "lnd-$1 pendingchannels: $pending"
|
||||
if [[ "$pending" == "0" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-lnd-sync(){
|
||||
while true; do
|
||||
if [[ "$(lncli-sim $1 getinfo 2>&1 | jq -r '.synced_to_chain' 2> /dev/null)" == "true" ]]; then
|
||||
echo "lnd-$1 is synced!"
|
||||
break
|
||||
fi
|
||||
echo "waiting for lnd-$1 to sync..."
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-clightning-channel(){
|
||||
while true; do
|
||||
pending=$(lightning-cli-sim $1 getinfo | jq -r '.num_pending_channels | length')
|
||||
echo "cln-$1 pendingchannels: $pending"
|
||||
if [[ "$pending" == "0" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait-for-clightning-sync(){
|
||||
while true; do
|
||||
if [[ ! "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.id' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
|
||||
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
|
||||
echo "cln-$1 is synced!"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "waiting for cln-$1 to sync..."
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
print_success() {
|
||||
printf "\033[;1;32mPASSED\033[;0m $1\n"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
printf "\033[;1;31mFAILED\033[;0m $1\n"
|
||||
}
|
||||
|
||||
run(){
|
||||
label=$1
|
||||
value=$2
|
||||
cmd=$3
|
||||
if [[ "$cmd" == "$value" ]]; then
|
||||
print_success "$label is $cmd"
|
||||
else
|
||||
print_error "$label is $cmd, should be $value"
|
||||
failed="true"
|
||||
fi
|
||||
}
|
||||
|
||||
failed="false"
|
||||
blockheight=213
|
||||
utxos=3
|
||||
channel_size=24000000 # 0.024 btc
|
||||
balance_size=12000000 # 0.012 btc
|
||||
|
||||
source $(pwd)/docker-scripts.sh
|
||||
lnbits-regtest-start
|
||||
echo "=================================="
|
||||
printf "\033[;1;36mregtest started! starting tests...\033[;0m\n"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
for i in 1 2 3; do
|
||||
run "lnd-$i .synced_to_chain" "true" $(lncli-sim $i getinfo | jq -r ".synced_to_chain")
|
||||
run "lnd-$i utxo count" $utxos $(lncli-sim $i listunspent | jq -r ".utxos | length")
|
||||
run "lnd-$i .block_height" $blockheight $(lncli-sim $i getinfo | jq -r ".block_height")
|
||||
if [[ "$i" == "1" ]]; then
|
||||
channel_count=6
|
||||
else
|
||||
channel_count=3
|
||||
fi
|
||||
run "lnd-$i openchannels" $channel_count $(lncli-sim $i listchannels | jq -r ".channels | length")
|
||||
run "lnd-$i .channels[0].capacity" $channel_size $(lncli-sim $i listchannels | jq -r ".channels[0].capacity")
|
||||
run "lnd-$i .channels[0].push_amount_sat" $balance_size $(lncli-sim $i listchannels | jq -r ".channels[0].push_amount_sat")
|
||||
done
|
||||
for i in 1 2 3; do
|
||||
# run "cln-$i blockheight" $blockheight $(lightning-cli-sim $i getinfo | jq -r ".blockheight")
|
||||
run "cln-$i utxo count" $utxos $(lightning-cli-sim $i listfunds | jq -r ".outputs | length")
|
||||
run "cln-$i openchannels" 2 $(lightning-cli-sim $i getinfo | jq -r ".num_active_channels")
|
||||
run "cln-$i channel[0].state" "CHANNELD_NORMAL" $(lightning-cli-sim $i listfunds | jq -r ".channels[0].state")
|
||||
run "cln-$i channel[0].amount_msat" $(($channel_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].amount_msat" | sed 's/msat//g')
|
||||
run "cln-$i channel[0].our_amount_msat" $(($balance_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].our_amount_msat" | sed 's/msat//g')
|
||||
done
|
||||
|
||||
run "eclair-1 openchannels" 2 $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
|
||||
run "eclair-1 blockHeight" $blockheight $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/getinfo -X POST -u :lnbits| jq '.blockHeight')
|
||||
run "lnbits service status" "200" $(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5001/")
|
||||
run "boltz service status" "200" $(curl -s -o /dev/null --head -w "%{http_code}" "http://localhost:9001/version")
|
||||
|
||||
# return non-zero exit code if a test fails
|
||||
if [[ "$failed" == "true" ]]; then
|
||||
echo ""
|
||||
echo "=================================="
|
||||
print_error "one more more tests failed"
|
||||
echo "=================================="
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
echo "=================================="
|
||||
print_success "all tests passed! yay!"
|
||||
echo "=================================="
|
||||
fi
|
||||
@@ -0,0 +1,41 @@
|
||||
ARG LNBITS_TAG=latest
|
||||
|
||||
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade
|
||||
RUN apt-get install -y ca-certificates curl gnupg netcat-openbsd git nodejs
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# install sparksidecar
|
||||
RUN git clone https://github.com/lnbits/spark_sidecar
|
||||
RUN cd spark_sidecar && npm ci
|
||||
|
||||
# Reinstall dependencies for lnbits just in case (needed for CMD usage)
|
||||
RUN uv sync --all-extras
|
||||
|
||||
# LNBITS
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
|
||||
ENV LNBITS_RESERVE_FEE_MIN="20000"
|
||||
ENV LNBITS_RESERVE_FEE_PERCENT="1"
|
||||
ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20"
|
||||
ENV FUNDING_SOURCE_MAX_RETRIES="10"
|
||||
|
||||
# spark sidecar
|
||||
ENV SPARK_NETWORK="MAINNET"
|
||||
ENV SPARK_SIDECAR_PORT="8765"
|
||||
ENV SPARK_PAY_WAIT_MS="20000"
|
||||
ENV SPARK_MULTIPLICITY="3"
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Entrypoint to start sparksidebar and LNbits
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
node spark_sidecar/server.mjs &
|
||||
|
||||
SIDECAR_PID=$!
|
||||
|
||||
# Wait for startup
|
||||
for i in {1..10}; do
|
||||
if nc -z localhost $SPARK_SIDECAR_PORT; then
|
||||
echo "sparksidebar is up!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for sparksidebar to start..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Optional: check if still not up
|
||||
if ! nc -z localhost $SPARK_SIDECAR_PORT; then
|
||||
echo "sparksidebar did not start successfully."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||
@@ -4,6 +4,7 @@ color_scheme: dark
|
||||
logo: "/logos/lnbits-full-inverse.png"
|
||||
search_enabled: true
|
||||
url: https://docs.lnbits.org
|
||||
markdown: gfm
|
||||
aux_links:
|
||||
"LNbits on GitHub":
|
||||
- "//github.com/lnbits/lnbits"
|
||||
|
||||
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 305 KiB |
@@ -5,8 +5,6 @@ title: API reference
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# API reference
|
||||
|
||||
API reference
|
||||
=============
|
||||
|
||||
[Swagger Docs](https://legend.lnbits.com/docs)
|
||||
[Swagger Docs](https://demo.lnbits.com/docs)
|
||||
|
||||
@@ -5,30 +5,39 @@ nav_order: 4
|
||||
has_children: true
|
||||
---
|
||||
|
||||
|
||||
For developers
|
||||
==============
|
||||
# For developers
|
||||
|
||||
Thanks for contributing :)
|
||||
|
||||
# Run
|
||||
|
||||
Run
|
||||
=====
|
||||
Follow the [Option 2 (recommended): UV](https://docs.lnbits.org/guide/installation.html)
|
||||
guide to install uv and other dependencies.
|
||||
|
||||
Then you can start LNbits uvicorn server with:
|
||||
|
||||
This starts the lnbits uvicorn server
|
||||
```bash
|
||||
poetry run lnbits
|
||||
uv run lnbits
|
||||
```
|
||||
|
||||
This starts the lnbits uvicorn with hot reloading.
|
||||
Or you can use the following to start uvicorn with hot reloading enabled:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
# or
|
||||
poetry run lnbits --reload
|
||||
uv run lnbits --reload
|
||||
```
|
||||
|
||||
Precommit hooks
|
||||
=====
|
||||
You might need the following extra dependencies on clean installation of Debian:
|
||||
|
||||
```
|
||||
sudo apt install nodejs
|
||||
sudo apt install npm
|
||||
npm install
|
||||
sudo apt-get install autoconf libtool libpg-dev
|
||||
```
|
||||
|
||||
# Precommit hooks
|
||||
|
||||
This ensures that all commits adhere to the formatting and linting rules.
|
||||
|
||||
@@ -36,31 +45,35 @@ This ensures that all commits adhere to the formatting and linting rules.
|
||||
make install-pre-commit-hook
|
||||
```
|
||||
|
||||
Tests
|
||||
=====
|
||||
# Tests
|
||||
|
||||
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
uv sync --all-extras --dev
|
||||
npm i
|
||||
```
|
||||
|
||||
Then to run the tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
Run formatting:
|
||||
|
||||
```bash
|
||||
make format
|
||||
```
|
||||
|
||||
Run mypy checks:
|
||||
|
||||
```bash
|
||||
poetry run mypy
|
||||
make mypy
|
||||
```
|
||||
|
||||
Run everything:
|
||||
|
||||
```bash
|
||||
make all
|
||||
```
|
||||
|
||||
@@ -5,11 +5,10 @@ title: Making extensions
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Extension set up
|
||||
=================
|
||||
# Extension set up
|
||||
|
||||
Start off by creating a fork of the [example extension](https://github.com/lnbits/example) into own GitHub repository and rename the repository to `mysuperplugin`:
|
||||
|
||||
```sh
|
||||
cd [my-working-folder]
|
||||
git clone https://github.com/[my-user-name]/mysuperplugin.git --depth=1 # Let's not use dashes or anything; it doesn't like those.
|
||||
@@ -18,6 +17,7 @@ rm -rf .git/
|
||||
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
|
||||
mv templates/example templates/mysuperplugin # Rename templates folder.
|
||||
```
|
||||
|
||||
- if you are on macOS and having difficulty with 'sed', consider `brew install gnu-sed` and use 'gsed', without -0 option after xargs.
|
||||
|
||||
1. Edit `manifest.json` and change the organisation name to your GitHub username.
|
||||
@@ -30,32 +30,24 @@ mv templates/example templates/mysuperplugin # Rename templates folder.
|
||||
1. ...
|
||||
1. Profit!!!
|
||||
|
||||
Extension structure explained
|
||||
-----------------------------
|
||||
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
|
||||
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
|
||||
* migrations.py: Create database tables for your plugin. They'll be created automatically when you start lnbits.
|
||||
## Extension structure explained
|
||||
|
||||
- views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
|
||||
- views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
|
||||
- migrations.py: Create database tables for your plugin. They'll be created automatically when you start lnbits.
|
||||
|
||||
... This document is a work-in-progress. Send pull requests if you get stuck, so others don't.
|
||||
|
||||
## Adding new dependencies
|
||||
|
||||
Adding new dependencies
|
||||
-----------------------
|
||||
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
|
||||
|
||||
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
|
||||
|
||||
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`:
|
||||
|
||||
```sh
|
||||
$ poetry add <package>
|
||||
```
|
||||
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty` or `uv`:
|
||||
|
||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
||||
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
Dependencies need to be added to `pyproject.toml`, then tested by running on `uv` and `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
|
||||
|
||||
SQLite to PostgreSQL migration
|
||||
-----------------------
|
||||
## SQLite to PostgreSQL migration
|
||||
|
||||
LNbits currently supports SQLite and PostgreSQL databases. There is a migration script `tools/conv.py` that helps users migrate from SQLite to PostgreSQL. This script also copies all extension databases to the new backend.
|
||||
|
||||
@@ -64,22 +56,31 @@ LNbits currently supports SQLite and PostgreSQL databases. There is a migration
|
||||
`mock_data.zip` contains a few lines of sample SQLite data and is used in automated GitHub test to see whether your migration in `conv.py` works. Run your extension and save a few lines of data into a SQLite `your_extension.sqlite3` file. Unzip `tests/data/mock_data.zip`, add `your_extension.sqlite3`, updated `database.sqlite3` and zip it again. Add the updated `mock_data.zip` to your PR.
|
||||
|
||||
### running migration locally
|
||||
|
||||
you will need a running postgres database
|
||||
|
||||
#### create lnbits user for migration database
|
||||
|
||||
```console
|
||||
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
|
||||
```
|
||||
|
||||
#### create migration database
|
||||
|
||||
```console
|
||||
sudo su - postgres -c "psql -c 'CREATE DATABASE migration;'"
|
||||
```
|
||||
|
||||
#### run the migration
|
||||
|
||||
```console
|
||||
make test-migration
|
||||
```
|
||||
|
||||
sudo su - postgres -c "psql -c 'CREATE ROLE lnbits LOGIN PASSWORD 'lnbits';'"
|
||||
|
||||
#### clean migration database afterwards, fails if you try again
|
||||
|
||||
```console
|
||||
sudo su - postgres -c "psql -c 'DROP DATABASE IF EXISTS migration;'"
|
||||
```
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
---
|
||||
layout: default
|
||||
title: FastAPI extension upgrade
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
## Defining a route with path parameters
|
||||
|
||||
**old:**
|
||||
|
||||
```python
|
||||
# with <>
|
||||
@offlineshop_ext.route("/lnurl/<item_id>", methods=["GET"])
|
||||
```
|
||||
|
||||
**new:**
|
||||
|
||||
```python
|
||||
# with curly braces: {}
|
||||
@offlineshop_ext.get("/lnurl/{item_id}")
|
||||
```
|
||||
|
||||
## Check if a user exists and access user object
|
||||
|
||||
**old:**
|
||||
|
||||
```python
|
||||
# decorators
|
||||
@check_user_exists()
|
||||
@@ -24,14 +34,18 @@ async def do_routing_stuff():
|
||||
**new:**
|
||||
If user doesn't exist, `Depends(check_user_exists)` will raise an exception.
|
||||
If user exists, `user` will be the user object
|
||||
|
||||
```python
|
||||
# depends calls
|
||||
@core_html_routes.get("/my_route")
|
||||
async def extensions(user: User = Depends(check_user_exists)):
|
||||
pass
|
||||
```
|
||||
|
||||
## Returning data from API calls
|
||||
|
||||
**old:**
|
||||
|
||||
```python
|
||||
return (
|
||||
{
|
||||
@@ -42,9 +56,11 @@ return (
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
```
|
||||
|
||||
FastAPI returns `HTTPStatus.OK` by default id no Exception is raised
|
||||
|
||||
**new:**
|
||||
|
||||
```python
|
||||
return {
|
||||
"id": wallet.wallet.id,
|
||||
@@ -54,6 +70,7 @@ return {
|
||||
```
|
||||
|
||||
To change the default HTTPStatus, add it to the path decorator
|
||||
|
||||
```python
|
||||
@core_app.post("/api/v1/payments", status_code=HTTPStatus.CREATED)
|
||||
async def payments():
|
||||
@@ -61,7 +78,9 @@ async def payments():
|
||||
```
|
||||
|
||||
## Raise exceptions
|
||||
|
||||
**old:**
|
||||
|
||||
```python
|
||||
return (
|
||||
{"message": f"Failed to connect to {domain}."},
|
||||
@@ -74,6 +93,7 @@ abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
|
||||
**new:**
|
||||
|
||||
Raise an exception to return a status code other than the default status code.
|
||||
|
||||
```python
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
@@ -82,7 +102,9 @@ raise HTTPException(
|
||||
```
|
||||
|
||||
## Extensions
|
||||
|
||||
**old:**
|
||||
|
||||
```python
|
||||
from quart import Blueprint
|
||||
|
||||
@@ -92,6 +114,7 @@ amilk_ext: Blueprint = Blueprint(
|
||||
```
|
||||
|
||||
**new:**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from lnbits.jinja2_templating import Jinja2Templates
|
||||
@@ -114,9 +137,12 @@ offlineshop_rndr = template_renderer([
|
||||
```
|
||||
|
||||
## Possible optimizations
|
||||
|
||||
### Use Redis as a cache server
|
||||
|
||||
Instead of hitting the database over and over again, we can store a short lived object in [Redis](https://redis.io) for an arbitrary key.
|
||||
Example:
|
||||
* Get transactions for a wallet ID
|
||||
* User data for a user id
|
||||
* Wallet data for a Admin / Invoice key
|
||||
|
||||
- Get transactions for a wallet ID
|
||||
- User data for a user id
|
||||
- Wallet data for a Admin / Invoice key
|
||||
@@ -1,29 +1,32 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Load the latest Swagger UI code and style from npm using unpkg.com -->
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css"/>
|
||||
<title>My New API</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div> <!-- Div to hold the UI component -->
|
||||
<script>
|
||||
window.onload = function () {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "https://legend.lnbits.com/openapi.json", //Location of Open API spec in the repo
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
})
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<!-- Load the latest Swagger UI code and style from npm using unpkg.com -->
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css"
|
||||
/>
|
||||
<title>My New API</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<!-- Div to hold the UI component -->
|
||||
<script>
|
||||
window.onload = function () {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: 'https://demo.lnbits.com/openapi.json', //Location of Open API spec in the repo
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl]
|
||||
})
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,14 +5,11 @@ title: Websockets
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
Websockets
|
||||
=================
|
||||
# Websockets
|
||||
|
||||
`websockets` are a great way to add a two way instant data channel between server and client.
|
||||
|
||||
LNbits has a useful in built websocket tool. With a websocket client connect to (obv change `somespecificid`) `wss://legend.lnbits.com/api/v1/ws/somespecificid` (you can use an online websocket tester). Now make a get to `https://legend.lnbits.com/api/v1/ws/somespecificid/somedata`. You can send data to that websocket by using `from lnbits.core.services import websocketUpdater` and the function `websocketUpdater("somespecificid", "somdata")`.
|
||||
|
||||
LNbits has a useful in built websocket tool. With a websocket client connect to (obv change `somespecificid`) `wss://demo.lnbits.com/api/v1/ws/somespecificid` (you can use an online websocket tester). Now make a get to `https://demo.lnbits.com/api/v1/ws/somespecificid/somedata`. You can send data to that websocket by using `from lnbits.core.services import websocketUpdater` and the function `websocketUpdater("somespecificid", "somdata")`.
|
||||
|
||||
Example vue-js function for listening to the websocket:
|
||||
|
||||
|
||||
@@ -1,76 +1,133 @@
|
||||
---
|
||||
layout: default
|
||||
title: Admin UI
|
||||
nav_order: 4
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
Admin UI
|
||||
========
|
||||
The LNbits Admin UI lets you change LNbits settings via the LNbits frontend.
|
||||
It is disabled by default and the first time you set the environment variable `LNBITS_ADMIN_UI=true`
|
||||
the settings are initialized and saved to the database and will be used from there as long the UI is enabled.
|
||||
From there on the settings from the database are used.
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
# LNBits Admin UI
|
||||
|
||||
Super User
|
||||
==========
|
||||
With the Admin UI we introduced the super user, it is created with the initialisation of the Admin UI and will be shown with a success message in the server logs.
|
||||
The super user has access to the server and can change settings that may crash the server and make it unresponsive via the frontend and api, like changing funding sources.
|
||||
[What you can do](#what-you-can-do-with-the-admin-ui) · [First Run](#first-run-and-super-user-id) · [Enable/Disable](#enabling-or-disabling-the-admin-ui) · [Reset](#reset-to-defaults) · [Allowed Users](#allowed-users) · [Guides](#additional-guides)
|
||||
|
||||
Also only the super user can brrrr satoshis to different wallets.
|
||||
**We introduced the Admin UI as the new default to make setup simpler and more straightforward**. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
|
||||
|
||||
The super user is only stored inside the settings table of the database and after the settings are "reset to defaults" and a restart happened,
|
||||
a new super user is created.
|
||||
<ins>On a fresh install the Admin UI is enabled by default</ins>, and at first launch you are prompted to create **Super User** credentials so that sensitive operations, such as switching funding sources, remain in trusted hands. When the Admin UI is enabled, configuration is written to and read from the database; for all settings managed by the UI, the parameters in `.env` are largely no longer used. If you disable the Admin UI, the `.env` file becomes the single source of truth again.
|
||||
|
||||
The super user is never sent over the api and the frontend only receives a bool if you are super user or not.
|
||||
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
|
||||
For a complete reference of legacy variables consult **[.env.example](../../.env.example)**.
|
||||
|
||||
We also added a decorator for the API routes to check for super user.
|
||||
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
|
||||
|
||||
There is also the possibility of posting the super user via webhook to another service when it is created. you can look it up here https://github.com/lnbits/lnbits/blob/main/lnbits/settings.py `class SaaSSettings`
|
||||
> [!WARNING]
|
||||
> Some settings remain `.env` only. Use **[.env.example](../../.env.example#L3-L87)** as the authoritative reference for those variables.
|
||||
|
||||
## What you can do with the Admin UI
|
||||
|
||||
Admin Users
|
||||
===========
|
||||
environment variable: `LNBITS_ADMIN_USERS`, comma-separated list of user ids
|
||||
Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessable. Also they have access to all the extension defined in `LNBITS_ADMIN_EXTENSIONS`.
|
||||
- Switch funding sources and other server level settings
|
||||
- Manage who can access LNbits (**[Allowed Users](#allowed-users)**)
|
||||
- Promote or demote Admin Users
|
||||
- Gate extensions to Admins only or disable them globally
|
||||
- Adjust balances with credit or debit
|
||||
- Adjust site customization
|
||||
|
||||
> [!NOTE]
|
||||
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
|
||||
|
||||
Allowed Users
|
||||
=============
|
||||
environment variable: `LNBITS_ALLOWED_USERS`, comma-separated list of user ids
|
||||
By defining this users, LNbits will no longer be usable by the public, only defined users and admins can then access the LNbits frontend.
|
||||
## First run and Super User ID
|
||||
|
||||
Setting this environment variable also disables account creation.
|
||||
Account creation can be also disabled by setting `LNBITS_ALLOW_NEW_ACCOUNTS=false`
|
||||
On first start with the Admin UI enabled you will be prompted to generate a Super User.
|
||||
|
||||
<img width="1573" height="976" alt="Admin_UI_first_install" src="https://github.com/user-attachments/assets/05aa634f-06ec-4a4d-a5c6-d90927c90991" />
|
||||
|
||||
How to activate
|
||||
=============
|
||||
```
|
||||
$ sudo systemctl stop lnbits.service
|
||||
$ cd ~/lnbits-legend
|
||||
$ sudo nano .env
|
||||
```
|
||||
-> set: `LNBITS_ADMIN_UI=true`
|
||||
If you need to read it from disk later:
|
||||
|
||||
Now start LNbits once in the terminal window
|
||||
```
|
||||
$ poetry run lnbits
|
||||
```
|
||||
You can now `cat` the Super User ID:
|
||||
```
|
||||
$ cat data/.super_user
|
||||
```bash
|
||||
cat /lnbits/data/.super_user
|
||||
# example
|
||||
123de4bfdddddbbeb48c8bc8382fe123
|
||||
```
|
||||
You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain.
|
||||
|
||||
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
|
||||
> [!WARNING]
|
||||
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
|
||||
|
||||
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
|
||||
|
||||
Do not forget
|
||||
```
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid.
|
||||
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
|
||||
|
||||
## Enabling or disabling the Admin UI
|
||||
|
||||
The Admin UI is enabled by default on new installs. To change the state:
|
||||
|
||||
1. Stop LNbits
|
||||
|
||||
```bash
|
||||
sudo systemctl stop lnbits.service
|
||||
```
|
||||
|
||||
2. Edit your `.env`
|
||||
|
||||
```
|
||||
cd ~/lnbits
|
||||
sudo nano .env
|
||||
```
|
||||
|
||||
3. Set one of
|
||||
|
||||
```
|
||||
# Enable Admin UI
|
||||
LNBITS_ADMIN_UI=true
|
||||
|
||||
# Disable Admin UI
|
||||
LNBITS_ADMIN_UI=false
|
||||
```
|
||||
|
||||
4. Start LNbits
|
||||
|
||||
```
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> With the Admin UI enabled, config is DB-backed and UI-managed settings ignore .env. Disable it to revert to [.env](../../.env.example) as the single source of truth.
|
||||
|
||||
## Reset to defaults
|
||||
|
||||
Using `Reset to defaults` in the Admin UI wipes stored settings. After a restart, a new `Super User` is created and the old one is no longer valid.
|
||||
|
||||
## Allowed Users
|
||||
|
||||
When set **at least one**, LNbits becomes private: only the listed users and Admins can access the frontend. Account creation is disabled automatically. You can also disable account creation explicitly.
|
||||
|
||||
<img width="1889" height="870" alt="grafik" src="https://github.com/user-attachments/assets/89011b75-a267-44ea-971a-1517968b7af5" />
|
||||
|
||||
> [!WARNING]
|
||||
> Assign your own account first when enabling **Allowed Users** to avoid locking yourself out. If you do get locked out, use your Super User to recover access.
|
||||
|
||||
## Additional Guides
|
||||
|
||||
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
||||
- **[User Roles](./user_roles.md)** — Overview of existing roles in LNbits.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What is available and how to configure each.
|
||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open source tools for building Bitcoin based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Extension Install
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Extension Install
|
||||
|
||||
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
|
||||
@@ -10,16 +16,16 @@ Go to `Manage Server` > `Server` > `Extensions Manifests`
|
||||
|
||||

|
||||
|
||||
|
||||
An `Extension Manifest` is a link to a `JSON` file which contains information about various extensions that can be installed (repository of extensions).
|
||||
Multiple repositories can be configured. For more information check the [Manifest File](https://github.com/lnbits/lnbits/blob/main/docs/guide/extension-install.md#manifest-file) section.
|
||||
|
||||
|
||||
**LNbits** administrators should configure their instances to use repositories that they trust (like the [lnbits-extensions](https://github.com/lnbits/lnbits-extensions/) one).
|
||||
|
||||
> **Warning**
|
||||
> Extensions can have bugs or malicious code, be careful what you install!!
|
||||
|
||||
## Install New Extension
|
||||
|
||||
Only administrator users can install or upgrade extensions.
|
||||
|
||||
Go to `Manage Extensions` > `Add Remove Extensions`
|
||||
@@ -45,13 +51,12 @@ Select the version to be installed (usually the last one) and click `Install`. O
|
||||
>
|
||||
> For Explicit Release: the order of the releases is the one in the "extensions" object
|
||||
|
||||
|
||||
The extension has been installed but it cannot be accessed yet. In order to activate the extension toggle it in the `Activated` state.
|
||||
|
||||
Go to `Manage Extensions` (as admin user or regular user). Search for the extension and enable it.
|
||||
|
||||
|
||||
## Uninstall Extension
|
||||
|
||||
On the `Install` page click `Manage` for the extension you want to uninstall:
|
||||

|
||||
|
||||
@@ -65,6 +70,7 @@ Users will no longer be able to access the extension.
|
||||
> The database for the extension is not removed. If the extension is re-installed later, the data will be accessible.
|
||||
|
||||
## Manifest File
|
||||
|
||||
The manifest file is just a `JSON` file that lists a collection of extensions that can be installed. This file is of the form:
|
||||
|
||||
```json
|
||||
@@ -77,30 +83,32 @@ The manifest file is just a `JSON` file that lists a collection of extensions th
|
||||
There are two ways to specify installable extensions:
|
||||
|
||||
### Explicit Release
|
||||
|
||||
It goes under the `extensions` object and it is of the form:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "lnurlp",
|
||||
"name": "LNURL Pay Links",
|
||||
"version": 1,
|
||||
"shortDescription": "Upgrade to version 111111111",
|
||||
"icon": "receipt",
|
||||
"details": "All charge names should be <code>111111111</code>. API panel must show: <br>",
|
||||
"archive": "https://github.com/lnbits/lnbits-extensions/raw/main/new/lnurlp/1/lnurlp.zip",
|
||||
"hash": "a22d02de6bf306a7a504cd344e032cc6d48837a1d4aeb569a55a57507bf9a43a",
|
||||
"htmlUrl": "https://github.com/lnbits/lnbits-extensions/tree/main/new/lnurlp/1",
|
||||
"infoNotification": "This is a very old version",
|
||||
"dependencies": ["other-ext-id"]
|
||||
}
|
||||
{
|
||||
"id": "lnurlp",
|
||||
"name": "LNURL Pay Links",
|
||||
"version": 1,
|
||||
"shortDescription": "Upgrade to version 111111111",
|
||||
"icon": "receipt",
|
||||
"details": "All charge names should be <code>111111111</code>. API panel must show: <br>",
|
||||
"archive": "https://github.com/lnbits/lnbits-extensions/raw/main/new/lnurlp/1/lnurlp.zip",
|
||||
"hash": "a22d02de6bf306a7a504cd344e032cc6d48837a1d4aeb569a55a57507bf9a43a",
|
||||
"htmlUrl": "https://github.com/lnbits/lnbits-extensions/tree/main/new/lnurlp/1",
|
||||
"infoNotification": "This is a very old version",
|
||||
"dependencies": ["other-ext-id"]
|
||||
}
|
||||
```
|
||||
|
||||
<details><summary>Fields Detailed Description</summary>
|
||||
|
||||
| Field | Type | | Description |
|
||||
|----------------------|---------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| id | string | mandatory | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
|
||||
| -------------------- | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | string | mandatory | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
|
||||
| name | string | mandatory | User friendly name for the extension. It will be displayed on the installation page. |
|
||||
| version | string | mandatory | Version of this release. [Semantic versioning](https://semver.org/) is recommended. |
|
||||
| version | string | mandatory | Version of this release. [Semantic versioning](https://semver.org/) is recommended. |
|
||||
| shortDescription | string | optional | A few words about the extension. It will be displayed on the installation page. |
|
||||
| icon | string | optional | quasar valid icon name |
|
||||
| details | string (html) | optional | Details about this particular release |
|
||||
@@ -109,31 +117,30 @@ It goes under the `extensions` object and it is of the form:
|
||||
| htmlUrl | string | optional | Link to the extension home page. |
|
||||
| infoNotification | string | optional | Users that have this release installed will see a info message for their extension. For example if the extension support will be terminated soon. |
|
||||
| criticalNotification | string | optional | Reserved for urgent notifications. The admin user will receive a message each time it visits the `Install` page. One example is if the extension has a critical bug. |
|
||||
| dependencies | list | optional | A list of extension IDs. It signals that those extensions must be installed BEFORE the this one can be installed.
|
||||
| dependencies | list | optional | A list of extension IDs. It signals that those extensions must be installed BEFORE the this one can be installed. |
|
||||
|
||||
</details>
|
||||
|
||||
This mode has the advantage of strictly specifying what releases of an extension can be installed.
|
||||
|
||||
### GitHub Repository
|
||||
|
||||
It goes under the `repos` object and it is of the form:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "withdraw",
|
||||
"organisation": "lnbits",
|
||||
"repository": "withdraw-extension"
|
||||
"id": "withdraw",
|
||||
"organisation": "lnbits",
|
||||
"repository": "withdraw-extension"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------|--------|-------------------------------------------------------|
|
||||
| id | string | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
|
||||
| organisation | string | The GitHub organisation (eg: `lnbits`) |
|
||||
| repository | string | The GitHub repository name (eg: `withdraw-extension`) |
|
||||
| Field | Type | Description |
|
||||
| ------------ | ------ | --------------------------------------------------------------------------------------------------- |
|
||||
| id | string | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
|
||||
| organisation | string | The GitHub organisation (eg: `lnbits`) |
|
||||
| repository | string | The GitHub repository name (eg: `withdraw-extension`) |
|
||||
|
||||
The admin user will see all releases from the Github repository:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ title: FAQ
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
|
||||
# FAQ - Frequently Asked Questions
|
||||
|
||||
## Install options
|
||||
|
||||
<ul><p>LNbits is not a node management software but a ⚡️LN only accounting system on top of a funding source.</p>
|
||||
|
||||
<details><summary>Funding my LNbits wallet from my node it doesn't work.</summary>
|
||||
@@ -54,6 +54,7 @@ allow-self-payment=1
|
||||
</ul>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<ul><details><summary>Message "https error" or network error" when scanning a LNbits QR</summary>
|
||||
<p>Bad news, this is a routing error that might have quite a lot of reasons. Let´s try a few of the most possible problems and their solutions. First choose your setup</p>
|
||||
<ul>
|
||||
@@ -83,7 +84,7 @@ allow-self-payment=1
|
||||
<details><summary>Wallet-URL deleted, are my funds safu ?</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<details><summary>Wallet on demo server legend.lnbits</summary>
|
||||
<details><summary>Wallet on demo server demo.lnbits.com</summary>
|
||||
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place. LNbits CANNOT help you to recover them when lost.</p>
|
||||
</details>
|
||||
</li>
|
||||
@@ -100,7 +101,7 @@ allow-self-payment=1
|
||||
|
||||
<details><summary>Configure a comment that people see when paying to my LNURLp QR</summary>
|
||||
<p>When you create a LNURL-p, by default the comment box is not filled. That means comments are not allowed to be attached to payments.<p>
|
||||
<p>In order to allow comments, add the characters lenght of the box, from 1 to 250. Once you put a number there,
|
||||
<p>In order to allow comments, add the characters length of the box, from 1 to 250. Once you put a number there,
|
||||
the comment box will be displayed in the payment process. You can also edit a LNURL-p already created and add that number.</p>
|
||||
|
||||

|
||||
@@ -150,12 +151,11 @@ allow-self-payment=1
|
||||
</details>
|
||||
|
||||
<details><summary>Can I configure a name to the payments i make?</summary>
|
||||
<p>In LNbits this is currently not possible to do - but to receive. This is only possible if the sender's LN wallet supports <a href="https://github.com/lnurl/luds">LUD-18</a> (nameDesc) like e.g. <a href="https://darthcoin.substack.com/p/obw-open-bitcoin-wallet">Open Bitcion Wallet - OBW</a> does. You will then see an alias/pseudonym in the details section of your LNbits transactions (click the arrows). Note that you can give any name there and it might not be related to the real sender´s name(!) if your receive such.</p>
|
||||
<p>In LNbits this is currently not possible to do - but to receive. This is only possible if the sender's LN wallet supports <a href="https://github.com/lnurl/luds">LUD-18</a> (nameDesc) like e.g. <a href="https://darthcoin.substack.com/p/obw-open-bitcoin-wallet">Open Bitcoin Wallet - OBW</a> does. You will then see an alias/pseudonym in the details section of your LNbits transactions (click the arrows). Note that you can give any name there and it might not be related to the real sender´s name(!) if your receive such.</p>
|
||||

|
||||
</p>
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary>How can I use a LNbits lndhub account in other wallet apps?</summary>
|
||||
<p>Open your LNbits with the account / wallet you want to use, go to "manage extensions" and activate the <a href="https://github.com/lnbits/lndhub">LNDHUB extension</a>.</p>
|
||||
<p>Then open the LNDHUB extension, choose the wallet you want to use and scan the QR code you want to use: "admin" or "invoice only", depending on the security level you want for that wallet.</p>
|
||||
@@ -166,6 +166,7 @@ allow-self-payment=1
|
||||
</ul>
|
||||
|
||||
## Building hardware tools
|
||||
|
||||
<ul> <p>LNbits has all sorts of open APIs and tools to program and connect to a lot of different devices for a gazillion of use-cases. Let us know what you did with it ! Come to the <a href="https://t.me/makerbits">Makerbits Telegram Group</a> if you are interested in building or if you need help with a project - we got you!</p>
|
||||
|
||||
<details><summary>ATM - deposit and withdraw in your shop or at your meetup</summary>
|
||||
@@ -216,6 +217,7 @@ allow-self-payment=1
|
||||
</ul>
|
||||
|
||||
## Use cases of LNbits
|
||||
|
||||
<ul><details><summary>Merchant</summary>
|
||||
<p>LNbits is a powerful solution for merchants, due to the easy setup with various extensions, that can be used for many scenarios.</p>
|
||||
<p><a href="https://darthcoin.substack.com/p/lnbits-for-small-merchants">Here is an overview of the LNbits tools available for a small restaurant as well as a hotel</a></p>
|
||||
@@ -262,6 +264,7 @@ allow-self-payment=1
|
||||
</ul>
|
||||
|
||||
## Developing for LNbits
|
||||
|
||||
<ul>
|
||||
<li><a href="https://docs.lnbits.org/devs/development.html">Making extensions / How to use Websockets / API reference</a></li>
|
||||
<li><a href="https://t.me/lnbits">Telegram LNbits Support Group</a></li></ul>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
layout: default
|
||||
title: Wallet comparison
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
# Backend Wallet Comparison Table
|
||||
|
||||
LNbits lets you choose **how your wallets are funded** — from fully self-custodial nodes to simple hosted services. You can switch the funding source **without touching your apps, users, or extensions**. That means you can start fast, learn, and later upgrade to more control and privacy when you are ready.
|
||||
|
||||
**Why this matters**
|
||||
|
||||
- **Flexibility:** Pick the backend that fits your skills and constraints today, change it later with minimal friction.
|
||||
- **Speed to ship:** Use a hosted option to get live quickly; move to a node when you need more control.
|
||||
- **Scalability:** Match cost and maintenance to your stage — from hobby to production.
|
||||
- **Privacy and compliance:** Choose between self-custody and provider-managed options depending on your requirements.
|
||||
|
||||
Below is a side-by-side comparison of Lightning funding sources you can use with LNbits.
|
||||
|
||||
> [!NOTE]
|
||||
> “Backend Wallet” and “Funding Source” mean the same thing — the wallet or service that funds your LNbits.
|
||||
|
||||
## LNbits Lightning Network Funding Sources Comparison Table
|
||||
|
||||
| **Funding Source** | **Custodial Type** | **KYC Required** | **Technical Knowledge Needed** | **Node Hosting Required** | **Privacy Level** | **Liquidity Management** | **Ease of Setup** | **Maintenance Effort** | **Cost Implications** | **Scalability** | **Notes** |
|
||||
| ------------------------------ | ------------------------ | ------------------- | ------------------------------ | ------------------------- | ----------------- | ------------------------ | ----------------- | ---------------------- | -------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------ |
|
||||
| **LND (gRPC)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | gRPC interface for LND; suitable for advanced integrations. |
|
||||
| **CoreLightning (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Requires setting up and managing your own CLN node. |
|
||||
| **Phoenixd** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Moderate | Low | Minimal fees | Medium | Mobile wallet backend; suitable for mobile integrations. |
|
||||
| **Nostr Wallet Connect (NWC)** | Custodial | Depends on provider | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur fees | Medium | Connects via Nostr protocol; depends on provider's policies. |
|
||||
| **Boltz** | Self-custodial | ❌ | Medium | ❌ | Medium | Provider-managed | Moderate | Moderate | Minimal fees | Medium | Uses submarine swaps; connects to Boltz client. |
|
||||
| **LND (REST)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for LND; suitable for web integrations. |
|
||||
| **CoreLightning REST** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for CLN; suitable for web integrations. |
|
||||
| **LNbits (another instance)** | Custodial | Depends on host | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur hosting fees | Medium | Connects to another LNbits instance; depends on host's policies. |
|
||||
| **Alby** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Browser extension wallet; suitable for web users. |
|
||||
| **Breez SDK** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Moderate | Low | Minimal fees | Medium | SDK for integrating Breez wallet functionalities. |
|
||||
| **OpenNode** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for merchants. |
|
||||
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
|
||||
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
||||
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
||||
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
|
||||
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
|
||||
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||
| **Eclair (ACINQ)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Connects via API; you run and manage your Eclair node. |
|
||||
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
|
||||
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
||||
|
||||
## Spark (L2)
|
||||
|
||||
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
|
||||
|
||||
### Notes for readers
|
||||
|
||||
- These are typical characteristics; your exact experience may vary by configuration and provider policy.
|
||||
- Pick based on your constraints: compliance (KYC), privacy, ops effort, and time-to-ship.
|
||||
|
||||
---
|
||||
|
||||
## Additional Guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||
- **[User Roles](./User_Roles.md)** — Quick Overview of existing Roles in LNBits.
|
||||
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
@@ -1,83 +1,277 @@
|
||||
---
|
||||
layout: default
|
||||
title: Basic installation
|
||||
nav_order: 2
|
||||
title: Installation
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
   [](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
# Basic installation
|
||||
|
||||
You can choose between four package managers, `poetry` and `nix`
|
||||
> [!NOTE]
|
||||
> **Default DB:** LNbits uses SQLite by default (simple & effective). You can switch to PostgreSQL — see the section below.
|
||||
|
||||
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
|
||||
## Table of contents
|
||||
|
||||
## Option 1 (recommended): poetry
|
||||
- [Option 1: AppImage (Linux)](#option-1-appimage-linux)
|
||||
- [Option 2: UV (recommended for developers)](#option-2-uv-recommended-for-developers)
|
||||
- [Option 2a (Legacy): Poetry — Replaced by UV](#option-2a-legacy-poetry--replaced-by-uv)
|
||||
- [Option 3: Install script (Debian/Ubuntu)](#option-3-install-script-debianubuntu)
|
||||
- [Option 4: Nix](#option-4-nix)
|
||||
- [Option 5: Docker](#option-5-docker)
|
||||
- [Option 6: Fly.io](#option-6-flyio)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Optional: PostgreSQL database](#optional-postgresql-database)
|
||||
- [Using LNbits](#using-lnbits)
|
||||
- [Additional guides](#additional-guides)
|
||||
- [Update LNbits (all methods)](#update-lnbits-all-methods)
|
||||
- [SQLite → PostgreSQL migration](#sqlite--postgresql-migration)
|
||||
- [LNbits as a systemd service](#lnbits-as-a-systemd-service)
|
||||
- [Reverse proxy with automatic HTTPS (Caddy)](#reverse-proxy-with-automatic-https-caddy)
|
||||
- [Apache2 reverse proxy over HTTPS](#apache2-reverse-proxy-over-https)
|
||||
- [Nginx reverse proxy over HTTPS](#nginx-reverse-proxy-over-https)
|
||||
- [HTTPS without a reverse proxy (self-signed)](#https-without-a-reverse-proxy-self-signed)
|
||||
- [LNbits on Umbrel behind Tor](#lnbits-on-umbrel-behind-tor)
|
||||
- [FreeBSD notes](#freebsd-notes)
|
||||
|
||||
Mininum poetry version has is ^1.2, but it is recommended to use latest poetry. (including OSX)
|
||||
## Option 1: AppImage (Linux)
|
||||
|
||||
**Quickstart**
|
||||
|
||||
1. Download latest AppImage from [releases](https://github.com/lnbits/lnbits/releases) **or** run:
|
||||
|
||||
```sh
|
||||
sudo apt-get install jq libfuse2
|
||||
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
|
||||
chmod +x LNbits-latest.AppImage
|
||||
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
|
||||
```
|
||||
|
||||
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||
|
||||
> [!NOTE]
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
## Option 2: UV (recommended for developers)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **It is recommended to use the latest version of UV & Make sure you have Python version 3.12 installed.**
|
||||
|
||||
### Verify Python
|
||||
|
||||
```sh
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### Install UV
|
||||
|
||||
```sh
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
### Install LNbits
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
git checkout main
|
||||
uv sync --all-extras
|
||||
|
||||
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
|
||||
sudo apt update
|
||||
sudo apt install software-properties-common
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt install python3.9 python3.9-distutils
|
||||
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command
|
||||
export PATH="/home/user/.local/bin:$PATH"
|
||||
# Next command, you can exchange with python3.10 or newer versions.
|
||||
# Identify your version with python3 --version and specify in the next line
|
||||
# command is only needed when your default python is not ^3.9 or ^3.10
|
||||
poetry env use python3.9
|
||||
poetry install --only main
|
||||
|
||||
mkdir data
|
||||
cp .env.example .env
|
||||
# set funding source amongst other options
|
||||
nano .env
|
||||
# Optional: set funding source and other options in .env (e.g., `nano .env`)
|
||||
```
|
||||
|
||||
### Run the server
|
||||
|
||||
```sh
|
||||
uv run lnbits
|
||||
# To change port/host: uv run lnbits --port 9000 --host 0.0.0.0
|
||||
# Add --debug to the command above and set DEBUG=true in .env for verbose output
|
||||
```
|
||||
|
||||
### LNbits CLI
|
||||
|
||||
```sh
|
||||
# Useful for superuser ID, updating extensions, etc.
|
||||
uv run lnbits-cli --help
|
||||
```
|
||||
|
||||
### Update LNbits
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
# Stop LNbits with Ctrl + X or your service manager
|
||||
# sudo systemctl stop lnbits
|
||||
|
||||
# Update code
|
||||
git pull --rebase
|
||||
|
||||
uv sync --all-extras
|
||||
uv run lnbits
|
||||
```
|
||||
|
||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||
|
||||
> [!NOTE]
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
||||
|
||||
<details>
|
||||
<summary><strong>Poetry install and update (legacy workflow)</summary>
|
||||
|
||||
This legacy section is preserved for older environments.
|
||||
**UV is the recommended (and faster) tool** for new installs. Use Poetry only if you have personal preferences or must support an older workflow.
|
||||
|
||||
> 
|
||||
> **It is recommended to use the latest version of Poetry & Make sure you have Python version 3.12 installed.**
|
||||
|
||||
### Verify Python version
|
||||
|
||||
```sh
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### Install Poetry
|
||||
|
||||
```sh
|
||||
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
|
||||
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
### Install LNbits
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
poetry env use 3.12
|
||||
git checkout main
|
||||
poetry install --only main
|
||||
cp .env.example .env
|
||||
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
|
||||
```sh
|
||||
poetry run lnbits
|
||||
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
|
||||
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
|
||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||
# To change port/host: poetry run lnbits --port 9000 --host 0.0.0.0
|
||||
# Add --debug to help troubleshooting (also set DEBUG=true in .env)
|
||||
```
|
||||
|
||||
#### LNbits CLI
|
||||
|
||||
```sh
|
||||
# A very useful terminal client for getting the superuser ID, updating extensions, etc.
|
||||
poetry run lnbits-cli --help
|
||||
```
|
||||
|
||||
#### Updating the server
|
||||
|
||||
```
|
||||
```sh
|
||||
cd lnbits
|
||||
# Stop LNbits with `ctrl + x`
|
||||
git pull
|
||||
# Keep your poetry install up to date, this can be done with `poetry self update`
|
||||
# Stop LNbits with Ctrl + X or with your service manager
|
||||
# sudo systemctl stop lnbits
|
||||
|
||||
# Update LNbits
|
||||
git pull --rebase
|
||||
|
||||
# Check your Poetry Python version
|
||||
poetry env list
|
||||
# If version is less than 3.12, update it:
|
||||
poetry env use python3.12
|
||||
poetry env remove python3.X
|
||||
poetry env list
|
||||
|
||||
# Reinstall and start
|
||||
poetry install --only main
|
||||
# Start LNbits with `poetry run lnbits`
|
||||
poetry run lnbits
|
||||
```
|
||||
|
||||
## Option 2: Nix
|
||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
|
||||
## Option 3: Install script (Debian/Ubuntu)
|
||||
|
||||
<details>
|
||||
<summary><strong>Show install script</strong> (one-line setup)</summary>
|
||||
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits.sh &&
|
||||
chmod +x lnbits.sh &&
|
||||
./lnbits.sh
|
||||
```
|
||||
|
||||
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
|
||||
## Option 4: Nix
|
||||
|
||||
<details>
|
||||
<summary><strong>Show Nix instructions</strong> (flakes, cachix, run)</summary>
|
||||
|
||||
```sh
|
||||
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
|
||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon --yes
|
||||
|
||||
# Enable nix-command and flakes experimental features for nix:
|
||||
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
|
||||
grep -qxF 'experimental-features = nix-command flakes' /etc/nix/nix.conf || \
|
||||
echo 'experimental-features = nix-command flakes' | sudo tee -a /etc/nix/nix.conf
|
||||
|
||||
# Add user to Nix
|
||||
grep -qxF "trusted-users = root $USER" /etc/nix/nix.conf || \
|
||||
echo "trusted-users = root $USER" | sudo tee -a /etc/nix/nix.conf
|
||||
|
||||
# Restart daemon so changes apply
|
||||
sudo systemctl restart nix-daemon
|
||||
|
||||
# Clone and build LNbits
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
|
||||
# Make data directory and persist data/extension folders
|
||||
mkdir data
|
||||
PROJECT_DIR="$(pwd)"
|
||||
{
|
||||
echo "export PYTHONPATH=\"$PROJECT_DIR/ns:\$PYTHONPATH\""
|
||||
echo "export LNBITS_DATA_FOLDER=\"$PROJECT_DIR/data\""
|
||||
echo "export LNBITS_EXTENSIONS_PATH=\"$PROJECT_DIR\""
|
||||
} >> ~/.bashrc
|
||||
grep -qxF '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ~/.bashrc || \
|
||||
echo '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' >> ~/.bashrc
|
||||
. ~/.bashrc
|
||||
|
||||
# Add cachix for cached binaries
|
||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
||||
cachix use lnbits
|
||||
|
||||
# Clone and build LNbits
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
# Build LNbits
|
||||
nix build
|
||||
|
||||
mkdir data
|
||||
```
|
||||
|
||||
#### Running the server
|
||||
@@ -89,42 +283,84 @@ nix run
|
||||
Ideally you would set the environment via the `.env` file,
|
||||
but you can also set the env variables or pass command line arguments:
|
||||
|
||||
``` sh
|
||||
```sh
|
||||
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
|
||||
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
||||
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000 --host 0.0.0.0
|
||||
|
||||
# Once you have created a user, you can set as the super_user
|
||||
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
|
||||
```
|
||||
|
||||
## Option 3: Docker
|
||||
> 
|
||||
> **Next steps**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
|
||||
## Option 5: Docker
|
||||
|
||||
<details>
|
||||
<summary><strong>Show Docker instructions</strong> (official image, volumes, extensions)</summary>
|
||||
|
||||
**Use latest image**
|
||||
|
||||
use latest version from docker hub
|
||||
```sh
|
||||
docker pull lnbits/lnbits
|
||||
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
|
||||
mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
docker run --detach --publish 5000:5000 --name lnbits \
|
||||
--volume ${PWD}/.env:/app/.env \
|
||||
--volume ${PWD}/data/:/app/data \
|
||||
lnbits/lnbits
|
||||
```
|
||||
build the image yourself
|
||||
|
||||
- The LNbits Docker image ships **without any extensions**; by default, any extensions you install are stored **inside the container** and will be **lost** when the container is removed, so you should set `LNBITS_EXTENSIONS_PATH` to a directory that’s **mapped to a persistent host volume** so extensions **survive rebuilds/recreates**—for example:
|
||||
|
||||
```sh
|
||||
docker run ... -e "LNBITS_EXTENSIONS_PATH='/app/data/extensions'" --volume ${PWD}/data/:/app/data ...
|
||||
```
|
||||
|
||||
**Build image yourself**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits/lnbits .
|
||||
cp .env.example .env
|
||||
mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
docker run --detach --publish 5000:5000 --name lnbits \
|
||||
--volume ${PWD}/.env:/app/.env \
|
||||
--volume ${PWD}/data/:/app/data \
|
||||
lnbits/lnbits
|
||||
```
|
||||
|
||||
## Option 4: Fly.io
|
||||
You can optionally override the install extras for both **Poetry** and **UV** to include optional features during build or setup:
|
||||
|
||||
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
|
||||
- with Poetry, pass extras via the `POETRY_INSTALL_ARGS` Docker build-arg (e.g., to enable the **Breez** funding source: `docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .`);
|
||||
- with UV, enable extras during environment sync (e.g., locally run `uv sync --extra breez` or `uv sync --all-extras`), and—**if your Dockerfile supports it**—you can mirror the same at build time via a build-arg such as `UV_SYNC_ARGS` (example pattern: `docker build --build-arg UV_SYNC_ARGS="--extra breez" -t lnbits/lnbits .`).
|
||||
|
||||
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
|
||||
**Enable Breez funding source at build**
|
||||
|
||||
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
|
||||
```sh
|
||||
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
||||
```
|
||||
|
||||
After install is complete, the command will output a command you should copy/paste/run to get `fly` into your `$PATH`. Something like:
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
|
||||
## Option 6: Fly.io
|
||||
|
||||
<details>
|
||||
<summary><strong>Deploy LNbits on Fly.io (free tier friendly)</summary>
|
||||
|
||||
**Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.**
|
||||
|
||||
1. Create an account at [Fly.io](https://fly.io).
|
||||
2. Install the Fly.io CLI ([guide](https://fly.io/docs/getting-started/installing-flyctl/)).
|
||||
|
||||
```
|
||||
flyctl was installed successfully to /home/ubuntu/.fly/bin/flyctl
|
||||
@@ -133,9 +369,9 @@ Manually add the directory to your $HOME/.bash_profile (or similar)
|
||||
export PATH="$FLYCTL_INSTALL/bin:$PATH"
|
||||
```
|
||||
|
||||
You can either run those commands, then `source ~/.bash_profile` or, if you don't, you'll have to call Fly from `~/.fly/bin/flyctl`.
|
||||
3. You can either run those commands, then `source ~/.bash_profile` or, if you don't, you'll have to call Fly from `~/.fly/bin/flyctl`.
|
||||
|
||||
Once installed, run the following commands.
|
||||
- Once installed, run the following commands.
|
||||
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
@@ -149,9 +385,16 @@ You'll be prompted to enter an app name, region, postgres (choose no), deploy no
|
||||
|
||||
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
|
||||
|
||||
Note: Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`. Environment variable strings should be quoted here, so if in `.env` you have `LNBITS_ENDPOINT=https://legend.lnbits.com` in `fly.toml` you should have `LNBITS_ENDPOINT="https://legend.lnbits.com"`.
|
||||
> 
|
||||
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
|
||||
> Environment variable strings should be quoted here. For example, if `.env` has
|
||||
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
|
||||
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
|
||||
|
||||
Note: Don't enter secret environment variables here. Fly.io offers secrets (via the `fly secrets` command) that are exposed as environment variables in your runtime. So, for example, if using the LND_REST funding source, you can run `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`.
|
||||
> 
|
||||
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
|
||||
> Example (LND REST funding source):
|
||||
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
||||
|
||||
```
|
||||
...
|
||||
@@ -199,33 +442,56 @@ Problems installing? These commands have helped us install LNbits.
|
||||
sudo apt install pkg-config libffi-dev libpq-dev
|
||||
|
||||
# build essentials for debian/ubuntu
|
||||
sudo apt install python3.9-dev gcc build-essential
|
||||
sudo apt install python3.10-dev gcc build-essential
|
||||
|
||||
# if the secp256k1 build fails:
|
||||
# if you used poetry
|
||||
poetry add setuptools wheel
|
||||
```
|
||||
|
||||
### Optional: PostgreSQL database
|
||||
> 
|
||||
>
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
|
||||
## Troubleshooting
|
||||
|
||||
```sh
|
||||
# on debian/ubuntu 'sudo apt-get -y install postgresql'
|
||||
# or follow instructions at https://www.postgresql.org/download/linux/
|
||||
sudo apt install pkg-config libffi-dev libpq-dev
|
||||
|
||||
# Postgres doesn't have a default password, so we'll create one.
|
||||
# build essentials (Debian/Ubuntu)
|
||||
sudo apt install python3.10-dev gcc build-essential
|
||||
|
||||
# if secp256k1 build fails and you used poetry
|
||||
poetry add setuptools wheel
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Optional: PostgreSQL database
|
||||
|
||||
> [!TIP]
|
||||
> If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and set up a database for LNbits.
|
||||
|
||||
```sh
|
||||
# Debian/Ubuntu: sudo apt-get -y install postgresql
|
||||
# or see https://www.postgresql.org/download/linux/
|
||||
|
||||
# Create a password for the postgres user
|
||||
sudo -i -u postgres
|
||||
psql
|
||||
# on psql
|
||||
ALTER USER postgres PASSWORD 'myPassword'; # choose whatever password you want
|
||||
# in psql
|
||||
ALTER USER postgres PASSWORD 'myPassword';
|
||||
\q
|
||||
# on postgres user
|
||||
# back as postgres user
|
||||
createdb lnbits
|
||||
exit
|
||||
```
|
||||
|
||||
You need to edit the `.env` file.
|
||||
**Configure LNbits**
|
||||
|
||||
```sh
|
||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
||||
@@ -236,46 +502,189 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits"
|
||||
|
||||
# Using LNbits
|
||||
|
||||
Now you can visit your LNbits at http://localhost:5000/.
|
||||
Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
|
||||
|
||||
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
|
||||
### Option A — First-run setup in the Browser (UI)
|
||||
|
||||
Then you can restart it and it will be using the new settings.
|
||||
1. On the **first start**, LNbits will **prompt you to Setup a SuperUser**.
|
||||
2. After creating it, you’ll be **redirected to the Admin UI as SuperUser**.
|
||||
3. In the Admin UI, **set your funding source** (backend wallet) and other preferences.
|
||||
4. **Restart LNbits** if prompted or after changing critical settings.
|
||||
|
||||
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
|
||||
> [!IMPORTANT]
|
||||
> Use the **SuperUser only** for initial setup and instance settings (funding source, configuration, Topup).
|
||||
> For maintenance, create a separate **Admin** account. For everyday usage (payments, wallets, etc.), **do not use the SuperUser** — use admin or regular user accounts instead. Its a bad behaviour.
|
||||
> Read more about [SuperUser](./super_user.md) and [Admin UI](./admin_ui.md)
|
||||
|
||||
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
|
||||
### Option B — Configure via `.env`
|
||||
|
||||
1. Edit your `.env` with preferred settings (funding, base URL, etc.).
|
||||
2. Set a funding source by configuring:
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`
|
||||
- plus the required credentials for your chosen backend (see **[wallets.md](./wallets.md)**).
|
||||
|
||||
3. **Restart LNbits** to apply changes.
|
||||
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
> **Paths overview**
|
||||
>
|
||||
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
|
||||
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
|
||||
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
||||
> - **Docker:** bind a host directory to `/app/data`.
|
||||
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
||||
> The container reads `/app/.env` (usually bind-mounted from your project root).
|
||||
|
||||
> [!TIP]
|
||||
> **Local Lightning test network**
|
||||
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
||||
> https://lightningpolar.com/
|
||||
|
||||
> [!TIP]
|
||||
> **API comparison before updates**
|
||||
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
|
||||
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
||||
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
||||
|
||||
# Additional guides
|
||||
|
||||
## SQLite to PostgreSQL migration
|
||||
If you already have LNbits installed and running, on an SQLite database, we **highly** recommend you migrate to postgres if you are planning to run LNbits on scale.
|
||||
## Update LNbits (all methods)
|
||||
|
||||
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user (see Postgres install guide above). Additionally, your LNbits instance should run once on postgres to implement the database schema before the migration works:
|
||||
> After updating, open **Admin UI → Extensions → “Update All”** to make sure extensions match the core version.
|
||||
|
||||
<details>
|
||||
<summary><strong>UV (recommended)</strong></summary>
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
uv sync --all-extras
|
||||
# restart (dev)
|
||||
uv run lnbits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Poetry (legacy)</strong></summary>
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
# Optional: ensure Python 3.12
|
||||
poetry env list
|
||||
poetry env use python3.12
|
||||
poetry install --only main
|
||||
# restart (dev)
|
||||
poetry run lnbits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>AppImage</strong></summary>
|
||||
|
||||
Download the latest AppImage from Releases and replace your old file **in the same directory** to keep the `./data` folder (DB, extensions).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Install script (Debian/Ubuntu)</strong></summary>
|
||||
|
||||
```sh
|
||||
# If you installed via lnbits.sh:
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
# then use your chosen runner (UV recommended)
|
||||
uv sync --all-extras
|
||||
uv run lnbits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Nix</strong></summary>
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
nix build
|
||||
# restart
|
||||
nix run
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Docker (official image)</strong></summary>
|
||||
|
||||
```sh
|
||||
docker pull lnbits/lnbits
|
||||
docker stop lnbits && docker rm lnbits
|
||||
docker run --detach --publish 5000:5000 --name lnbits \
|
||||
--volume ${PWD}/.env:/app/.env \
|
||||
--volume ${PWD}/data/:/app/data \
|
||||
lnbits/lnbits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Docker (build yourself)</strong></summary>
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
docker build -t lnbits/lnbits .
|
||||
docker stop lnbits && docker rm lnbits
|
||||
docker run --detach --publish 5000:5000 --name lnbits \
|
||||
--volume ${PWD}/.env:/app/.env \
|
||||
--volume ${PWD}/data/:/app/data \
|
||||
lnbits/lnbits
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Fly.io</strong></summary>
|
||||
|
||||
```sh
|
||||
# If using Dockerfile in repo (recommended)
|
||||
cd lnbits
|
||||
git pull --rebase
|
||||
fly deploy
|
||||
# Logs & shell if needed
|
||||
fly logs
|
||||
fly ssh console
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## SQLite → PostgreSQL migration
|
||||
|
||||
> [!TIP]
|
||||
> If you run on SQLite and plan to scale, migrate to Postgres.
|
||||
|
||||
```sh
|
||||
# STOP LNbits
|
||||
|
||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
||||
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
|
||||
# Edit .env with Postgres URL
|
||||
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
||||
# save and exit
|
||||
|
||||
# START LNbits
|
||||
# STOP LNbits
|
||||
poetry run python tools/conv.py
|
||||
# START then STOP LNbits once to apply schema
|
||||
uv run python tools/conv.py
|
||||
# or
|
||||
make migration
|
||||
```
|
||||
|
||||
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
|
||||
|
||||
- Launch LNbits again and verify.
|
||||
|
||||
## LNbits as a systemd service
|
||||
|
||||
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
|
||||
Create `/etc/systemd/system/lnbits.service`:
|
||||
|
||||
```
|
||||
# Systemd unit for lnbits
|
||||
@@ -283,17 +692,14 @@ Systemd is great for taking care of your LNbits instance. It will start it on bo
|
||||
|
||||
[Unit]
|
||||
Description=LNbits
|
||||
# you can uncomment these lines if you know what you're doing
|
||||
# it will make sure that lnbits starts after lnd (replace with your own backend service)
|
||||
# Optional: start after your backend
|
||||
#Wants=lnd.service
|
||||
#After=lnd.service
|
||||
|
||||
[Service]
|
||||
# replace with the absolute path of your lnbits installation
|
||||
WorkingDirectory=/home/lnbits/lnbits
|
||||
# same here. run `which poetry` if you can't find the poetry binary
|
||||
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
|
||||
# replace with the user that you're running lnbits on
|
||||
# Find uv path via `which uv`
|
||||
ExecStart=/home/lnbits/.local/bin/uv run lnbits
|
||||
User=lnbits
|
||||
Restart=always
|
||||
TimeoutSec=120
|
||||
@@ -304,73 +710,47 @@ Environment=PYTHONUNBUFFERED=1
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Save the file and run the following commands:
|
||||
Enable & start:
|
||||
|
||||
```sh
|
||||
sudo systemctl enable lnbits.service
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
|
||||
## Reverse proxy with automatic HTTPS using Caddy
|
||||
## Reverse proxy with automatic HTTPS (Caddy)
|
||||
|
||||
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
|
||||
Point your domain A-record to your server IP. Install Caddy: [Caddy install guide](https://caddyserver.com/docs/install#debian-ubuntu-raspbian)
|
||||
|
||||
Point your domain at the IP of the server you're running LNbits on, by making an `A` record.
|
||||
|
||||
Install Caddy on the server
|
||||
https://caddyserver.com/docs/install#debian-ubuntu-raspbian
|
||||
|
||||
```
|
||||
```sh
|
||||
sudo caddy stop
|
||||
```
|
||||
|
||||
Create a Caddyfile
|
||||
|
||||
```
|
||||
sudo nano Caddyfile
|
||||
```
|
||||
|
||||
Assuming your LNbits is running on port `5000` add:
|
||||
Add:
|
||||
|
||||
```
|
||||
yourdomain.com {
|
||||
handle /api/v1/payments/sse* {
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
transport http {
|
||||
keepalive off
|
||||
compression off
|
||||
}
|
||||
}
|
||||
}
|
||||
reverse_proxy 0.0.0.0:5000 {
|
||||
header_up X-Forwarded-Host yourdomain.com
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Save and exit `CTRL + x`
|
||||
Save (Ctrl+X) and start:
|
||||
|
||||
```
|
||||
```sh
|
||||
sudo caddy start
|
||||
```
|
||||
|
||||
## Running behind an Apache2 reverse proxy over HTTPS
|
||||
|
||||
Install Apache2 and enable Apache2 mods:
|
||||
## Apache2 reverse proxy over HTTPS
|
||||
|
||||
```sh
|
||||
apt-get install apache2 certbot
|
||||
a2enmod headers ssl proxy proxy-http
|
||||
```
|
||||
|
||||
Create a SSL certificate with LetsEncrypt:
|
||||
|
||||
```sh
|
||||
a2enmod headers ssl proxy proxy_http
|
||||
certbot certonly --webroot --agree-tos --non-interactive --webroot-path /var/www/html -d lnbits.org
|
||||
```
|
||||
|
||||
Create an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
|
||||
Create `/etc/apache2/sites-enabled/lnbits.conf`:
|
||||
|
||||
```sh
|
||||
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
|
||||
@@ -397,27 +777,20 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
|
||||
EOF
|
||||
```
|
||||
|
||||
Restart Apache2:
|
||||
Restart:
|
||||
|
||||
```sh
|
||||
service restart apache2
|
||||
service apache2 restart
|
||||
```
|
||||
|
||||
## Running behind an Nginx reverse proxy over HTTPS
|
||||
|
||||
Install nginx:
|
||||
## Nginx reverse proxy over HTTPS
|
||||
|
||||
```sh
|
||||
apt-get install nginx certbot
|
||||
```
|
||||
|
||||
Create a SSL certificate with LetsEncrypt:
|
||||
|
||||
```sh
|
||||
certbot certonly --nginx --agree-tos -d lnbits.org
|
||||
```
|
||||
|
||||
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
|
||||
Create `/etc/nginx/sites-enabled/lnbits.org`:
|
||||
|
||||
```sh
|
||||
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
|
||||
@@ -432,6 +805,15 @@ server {
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
|
||||
listen [::]:443 ssl;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
|
||||
@@ -442,78 +824,71 @@ server {
|
||||
EOF
|
||||
```
|
||||
|
||||
Restart nginx:
|
||||
Restart:
|
||||
|
||||
```sh
|
||||
service restart nginx
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
## Using https without reverse proxy
|
||||
The most common way of using LNbits via https is to use a reverse proxy such as Caddy, nginx, or ngriok. However, you can also run LNbits via https without additional software. This is useful for development purposes or if you want to use LNbits in your local network.
|
||||
---
|
||||
|
||||
We have to create a self-signed certificate using `mkcert`. Note that this certificate is not "trusted" by most browsers but that's fine (since you know that you have created it) and encryption is always better than clear text.
|
||||
## HTTPS without a reverse proxy (self-signed)
|
||||
|
||||
#### Install mkcert
|
||||
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
|
||||
Create a self-signed cert (useful for local/dev). Browsers won’t trust it by default.
|
||||
|
||||
### Install mkcert
|
||||
|
||||
- Install instructions: [mkcert README](https://github.com/FiloSottile/mkcert)
|
||||
- Ubuntu example:
|
||||
|
||||
Install mkcert on Ubuntu:
|
||||
```sh
|
||||
sudo apt install libnss3-tools
|
||||
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
|
||||
chmod +x mkcert-v*-linux-amd64
|
||||
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
|
||||
```
|
||||
#### Create certificate
|
||||
To create a certificate, first `cd` into your LNbits folder and execute the following command on Linux:
|
||||
|
||||
### Create certificate
|
||||
|
||||
**OpenSSL**
|
||||
|
||||
```sh
|
||||
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.pem -keyout key.pem
|
||||
```
|
||||
This will create two new files (`key.pem` and `cert.pem `).
|
||||
|
||||
Alternatively, you can use mkcert ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/)):
|
||||
**mkcert** (alternative)
|
||||
|
||||
```sh
|
||||
# add your local IP (192.x.x.x) as well if you want to use it in your local network
|
||||
# include your local IP (e.g., 192.x.x.x) if needed
|
||||
mkcert localhost 127.0.0.1 ::1
|
||||
```
|
||||
|
||||
You can then pass the certificate files to uvicorn when you start LNbits:
|
||||
**Run with certs**
|
||||
|
||||
```sh
|
||||
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
|
||||
```
|
||||
|
||||
## LNbits on Umbrel behind Tor
|
||||
|
||||
## LNbits running on Umbrel behind Tor
|
||||
See this community [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604).
|
||||
|
||||
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
|
||||
## FreeBSD notes
|
||||
|
||||
## Docker installation
|
||||
Issue with secp256k1 0.14.0 on FreeBSD (thanks @GitKalle):
|
||||
|
||||
To install using docker you first need to build the docker image as:
|
||||
1. Install `py311-secp256k1` with `pkg install py311-secp256k1`.
|
||||
2. Change version in `pyproject.toml` from `0.14.0` to `0.13.2`.
|
||||
3. Rewrite `poetry.lock` with `poetry lock`.
|
||||
4. Follow install instructions with Poetry.
|
||||
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits-legend .
|
||||
```
|
||||
---
|
||||
|
||||
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
|
||||
## Powered by LNbits
|
||||
|
||||
```
|
||||
cp <lnbits_repo>/.env.example .env
|
||||
```
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
and change the configuration in `.env` as required.
|
||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.lnbits.com)
|
||||
|
||||
Then create the data directory
|
||||
```
|
||||
mkdir data
|
||||
```
|
||||
|
||||
Then the image can be run as:
|
||||
|
||||
```
|
||||
docker run --detach --publish 5000:5000 --name lnbits-legend -e "LNBITS_BACKEND_WALLET_CLASS='FakeWallet'" --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
|
||||
```
|
||||
|
||||
Finally you can access your lnbits on your machine at port 5000.
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/) [](https://my.lnbits.com/login) [](https://news.lnbits.com/) [](https://extensions.lnbits.com/)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Generic OIDC Authentication Configuration
|
||||
|
||||
This document explains how to configure generic OIDC authentication for LNbits, which allows integration with various OIDC-compliant authentication providers such as Zitadel, Authentik, and others.
|
||||
|
||||
## Overview
|
||||
|
||||
The generic OIDC provider (`oidc`) complements the existing Keycloak provider and allows you to integrate any OIDC-compliant authentication service. You can customize the login button with your own organization name and icon.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the following environment variables to your `.env` file or system environment:
|
||||
|
||||
### Required Settings
|
||||
|
||||
```bash
|
||||
# Enable OIDC authentication
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
|
||||
# OIDC Discovery URL (well-known endpoint)
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||
|
||||
# Client credentials from your OIDC provider
|
||||
LNBITS_OIDC_CLIENT_ID=your-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
### Optional Settings - Customize the Login Button
|
||||
|
||||
You can customize how the OIDC login button appears to your users:
|
||||
|
||||
```bash
|
||||
# Custom organization name (displayed on the login button)
|
||||
# Example: "Login via Zitadel" or "Login via Authentik"
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||
|
||||
# Custom icon URL (displayed on the login button)
|
||||
# Can be a full URL or a path to a local image
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ICON=https://zitadel.com/favicon.svg
|
||||
```
|
||||
|
||||
If not set, the button will display "Login via OIDC" with a generic lock icon.
|
||||
|
||||
## Zitadel Configuration Example
|
||||
|
||||
For Zitadel, configure as follows:
|
||||
|
||||
1. Create a new application in Zitadel
|
||||
2. Choose "Web" application type
|
||||
3. Configure the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
4. Save the Client ID and Client Secret
|
||||
5. Use these environment variables:
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||
LNBITS_OIDC_CLIENT_ID=your-zitadel-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||
# Customize the button to show "Login via Zitadel" with Zitadel's logo
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ICON="https://zitadel.com/favicon.svg"
|
||||
```
|
||||
|
||||
**Result**: The login page will display a button with the text "Login via Zitadel" and the Zitadel logo.
|
||||
|
||||
## Authentik Configuration Example
|
||||
|
||||
For Authentik:
|
||||
|
||||
1. Create a new OAuth2/OpenID Provider
|
||||
2. Set the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
3. Configure scopes: `openid`, `email`, `profile`
|
||||
4. Get the Client ID and Client Secret
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||
LNBITS_OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/your-app/.well-known/openid-configuration
|
||||
LNBITS_OIDC_CLIENT_ID=your-authentik-client-id
|
||||
LNBITS_OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Authentik"
|
||||
```
|
||||
|
||||
## Multiple Auth Methods
|
||||
|
||||
You can enable multiple authentication methods simultaneously:
|
||||
|
||||
```bash
|
||||
LNBITS_AUTH_ALLOWED_METHODS=username-password,oidc-auth,keycloak-auth
|
||||
```
|
||||
|
||||
## Discovery Endpoint Requirements
|
||||
|
||||
Your OIDC provider must expose a standard discovery endpoint (`.well-known/openid-configuration`) that includes:
|
||||
|
||||
- `authorization_endpoint`
|
||||
- `token_endpoint`
|
||||
- `userinfo_endpoint`
|
||||
- `jwks_uri` (JSON Web Key Set)
|
||||
|
||||
The OIDC implementation will automatically fetch these endpoints from the discovery URL.
|
||||
|
||||
## User Mapping
|
||||
|
||||
The OIDC provider maps user information from the OIDC userinfo endpoint:
|
||||
|
||||
- `sub` → User ID
|
||||
- `email` → Email address
|
||||
- `given_name` → First name
|
||||
- `family_name` → Last name
|
||||
- `name` or `preferred_username` → Display name
|
||||
- `picture` → Profile picture URL
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication fails
|
||||
|
||||
1. Verify the discovery URL is accessible
|
||||
2. Check that Client ID and Client Secret are correct
|
||||
3. Ensure redirect URI in your OIDC provider matches: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||
4. Check LNbits logs for detailed error messages
|
||||
|
||||
### User info not populated
|
||||
|
||||
Some OIDC providers may use different claim names. If user information is not correctly populated, check your provider's userinfo endpoint response format and adjust the provider class if needed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always use HTTPS in production
|
||||
- Keep client secrets secure and never commit them to version control
|
||||
- Use environment variables or secure configuration management
|
||||
- Regularly rotate client secrets
|
||||
- Review OIDC provider's security best practices
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The OIDC provider is implemented in `lnbits/core/models/sso/oidc.py` and extends the `fastapi_sso` library's `SSOBase` class. It uses the standard OpenID Connect flow with:
|
||||
|
||||
- Scopes: `openid`, `email`, `profile`
|
||||
- Response type: `code` (authorization code flow)
|
||||
- Discovery document for automatic endpoint resolution
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
layout: default
|
||||
title: Super user
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
# LNbits Super User (SU)
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
- [What is the Super User?](#what-is-the-super-user)
|
||||
- [When is the Super User created?](#when-is-the-super-user-created)
|
||||
- [Disabeling the Admin UI](#disabeling-the-admin-ui)
|
||||
- [Super User identity and storage](#super-user-identity-and-storage)
|
||||
- [Security model since v1](#security-model-since-v1)
|
||||
- [Admin vs Super User](#admin-vs-super-user)
|
||||
- [Operational guidance](#operational-guidance)
|
||||
- [Additional guides](#additional-guides)
|
||||
|
||||
<details>
|
||||
<summary><strong>TLDR</strong></summary>
|
||||
|
||||
- **No Admin UI → No Super User.** The Super User (SU) exists only when `LNBITS_ADMIN_UI=true`.
|
||||
- **Why SU exists:** SU can do a few high impact actions regular admins cannot, like **changing the funding source**, **restarting the server from the UI**, and **crediting or debiting accounts**.
|
||||
- **Login changes since v1:** Logging in by **user ID** for SU and admins is **disabled**. On first visit after enabling the Admin UI you will be prompted to set a **username and password** for the SU.
|
||||
- **Trust model:** Admins and the SU share about **99 percent of the same powers**, but the SU is the one trusted with funding source control and cannot be demoted by regular admins.
|
||||
|
||||
</details>
|
||||
|
||||
## What is the Super User?
|
||||
|
||||
The **Super User** is the owner-operator account of an LNbits instance. Think of it as your “break glass” operator with a few capabilities that are intentionally reserved for the person ultimately responsible for the server and the funding rails.
|
||||
|
||||
The SU is created alongside the [Admin UI](./admin_ui.md) and is meant to keep enviroment operations pleasant in the UI while keeping the most sensitive knobs in trusted hands.
|
||||
|
||||
**Key SU capabilities**
|
||||
|
||||
- **Change the funding source** for the instance
|
||||
- **Restart the LNbits server** from the web UI
|
||||
- **Credit or debit accounts** for operational corrections
|
||||
|
||||
> Note
|
||||
> These are separated from regular admin tasks on purpose. It helps maintain least privilege and reduces the chance of accidental or malicious changes.
|
||||
|
||||
## Admin vs Super User
|
||||
|
||||
| Capability | Admin | Super User |
|
||||
| ------------------------ | ---------- | ---------- |
|
||||
| View Admin UI | If enabled | If enabled |
|
||||
| Change funding source | — | ✓ |
|
||||
| Credit or debit accounts | — | ✓ |
|
||||
| Restart server from UI | — | ✓ |
|
||||
| Manage users and wallets | ✓ | ✓ |
|
||||
| Instance-level settings | ✓ | ✓ |
|
||||
| Manage notifications | ✓ | ✓ |
|
||||
| Exchange rates | ✓ | ✓ |
|
||||
| View all Payments | ✓ | ✓ |
|
||||
|
||||
**Why both roles?**
|
||||
In many teams the person running the server prefers to **delegate day-to-day admin work** while keeping funding and final authority safe. Admins can do almost everything; the SU retains the last few high risk powers.
|
||||
|
||||
## When is the Super User created?
|
||||
|
||||
- The SU is created **only** when you enable the Admin UI: `LNBITS_ADMIN_UI=true`.
|
||||
- If the Admin UI is **disabled**, there is **no SU** and all SU-only UI is hidden.
|
||||
|
||||
## Disabeling the Admin UI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Read the [Admin UI guide](./admin_ui.md) before Disabeling. You are turning on a management surface; do it deliberately.
|
||||
|
||||
Set the environment variable in your deployment:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
LNBITS_ADMIN_UI=false
|
||||
```
|
||||
|
||||
## Super User identity and storage
|
||||
|
||||
LNbits stores the **Super User ID** at:
|
||||
|
||||
```
|
||||
/lnbits/data/.super_user
|
||||
```
|
||||
|
||||
- Back this up along with the rest of `/lnbits/data` as part of your secure backup routine.
|
||||
- **Changing who is the SU** can only be done by someone with **CLI access to the host OS** where LNbits runs. **Regular admins cannot revoke or replace the SU in the Admin UI.**
|
||||
|
||||
## Security model since v1
|
||||
|
||||
- **User-ID logins are disabled** for SU and admin roles.
|
||||
- **Credentialed login is required:** set a **username and password** for the SU at first run of the Admin UI.
|
||||
- **SU secrecy:** Regular users and admins **cannot discover the SU user ID** through normal UI flows.
|
||||
|
||||
## Operational guidance
|
||||
|
||||
These are practical tips for running a safe and friendly instance.
|
||||
|
||||
- It is normal to **delegate admin** duties to trusted people. Admins have about **99 percent** of SU powers for day-to-day work.
|
||||
- Keep the **SU** reserved for the person legally or operationally responsible for the **funding source**.
|
||||
- Use admin roles for regular day-to-day management and keep the SU for reserved SU tasks only.
|
||||
|
||||
## Additional guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
|
||||
- **[User Roles](./user_roles.md)** — Overview of roles and what they can do.
|
||||
- **[Funding sources](./funding-sources-table.md)** — Available options and how to enable and configure them.
|
||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
layout: default
|
||||
title: User Roles
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
# LNbits Roles: A Quick Overview
|
||||
|
||||
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
|
||||
|
||||
**Jump to:**
|
||||
[Roles at a Glance](#roles-at-a-glance) •
|
||||
[Super User](#super-user--master-control) •
|
||||
[Admin](#admin--day-to-day-manager) •
|
||||
[Regular User](#regular-user--everyday-use) •
|
||||
[Best Practices](#best-practices) •
|
||||
[Additional Guides](#additional-guides)
|
||||
|
||||
---
|
||||
|
||||
## Roles at a Glance
|
||||
|
||||
| Capability | **Super User** (owner) | **Admin** (manager) | **Regular User** (end user) |
|
||||
| -------------------------------- | :--------------------: | :-----------------: | :-------------------------: |
|
||||
| Change **funding source** | ✅ | ❌ | ❌ |
|
||||
| Credit/Debit any wallet | ✅ | ❌ | ❌ |
|
||||
| Manage Admins & Users | ✅ | ✅ | ❌ |
|
||||
| Enable/disable extensions | ✅ | ✅ | ❌ |
|
||||
| Use wallets & allowed extensions | ✅ | ✅ | ✅ |
|
||||
|
||||
> **Plain talk:** **Super User** = Owner • **Admin** = Trusted manager • **Regular User** = End user
|
||||
|
||||
## Role Snapshots
|
||||
|
||||
### Super User — Master Control
|
||||
|
||||
For initial setup and rare, high-impact changes.
|
||||
|
||||
- Configure **server-level settings** (e.g., funding source).
|
||||
- **Credit/Debit** any wallet.
|
||||
- Create and manage initial **Admin(s)**.
|
||||
|
||||
> **Sign-in:** username + password (v1+). The old query-string login is retired.
|
||||
|
||||
### Admin — Day-to-Day Manager
|
||||
|
||||
For running the service without touching the most sensitive knobs.
|
||||
|
||||
- Manage **Users**, **Admins**, and **Extensions** in the Admin UI.
|
||||
- Adjust security-related settings (e.g., `rate_limiter`, `ip_blocker`).
|
||||
- Handle operations settings (e.g., `service_fee`, `invoice_expiry`).
|
||||
- Build brand design in **Site Customization**.
|
||||
- Update user accounts.
|
||||
|
||||
**Typical tasks:** onboarding users, enabling extensions, tidying wallets, reviewing activity.
|
||||
|
||||
> **Sign-in:** username + password (v1+). The old query-string login is retired.
|
||||
|
||||
### Regular User — Everyday Use
|
||||
|
||||
For using LNbits, not administering it.
|
||||
|
||||
- Access **personal wallets** and **allowed extensions**.
|
||||
- No server/admin privileges.
|
||||
|
||||
**Typical tasks:** receive and send payments, use enabled extensions.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Minimize risk:** Reserve **Super User** for rare, sensitive actions (funding source, debit/credit). Use **Admin** for daily operations.
|
||||
- **Keep access tidy:** Review your Admin list occasionally; remove unused accounts.
|
||||
- **Change management:** Test risky changes (like funding) in a staging setup first.
|
||||
|
||||
## Additional Guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||
- **[Super User](./super_user.md)** — Deep dive on responsibilities and safe usage patterns.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems—fast, free, and extendable.
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
@@ -4,97 +4,394 @@ title: Backend wallets
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
Backend wallets
|
||||
===============
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
|
||||
LNbits can run on top of many lightning-network funding sources with more being added regularly.
|
||||
# Backend wallets
|
||||
|
||||
A backend wallet can be configured using the following LNbits environment variables:
|
||||
**LNbits is modular**: You can switch the funding source (backend wallet) **without changing anything else** in your setup. Keep your extensions, apps, users, and config as-is — just point LNbits to a different backend via environment variables.
|
||||
|
||||
**What stays the same when you switch backends**
|
||||
|
||||
### CoreLightning
|
||||
- Your LNbits setup and extensions
|
||||
- Your API keys and endpoints
|
||||
- Your server and deployment setup
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
||||
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
A backend wallet is selected and configured entirely through LNbits environment variables. See the options and variables below, and compare them here: [Funding-Source-Table.md](funding-sources-table.md)
|
||||
|
||||
### CoreLightning REST
|
||||
> [!NOTE]
|
||||
> **Terminology:** “Backend Wallet” and “Funding Source” mean the same thing — the wallet or service that funds your LNbits.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
|
||||
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
|
||||
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
||||
- `CORELIGHTNING_REST_CERT`: /home/lightning/clnrest/tls.cert
|
||||
## Funding Sources
|
||||
|
||||
### Spark (Core Lightning)
|
||||
## Funding Sources
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
|
||||
- `SPARK_URL`: http://10.147.17.230:9737/rpc
|
||||
- `SPARK_TOKEN`: secret_access_key
|
||||
| | | |
|
||||
| ----------------------------------------------- | ------------------------------------- | ------------------------------------------------- |
|
||||
| [CLNRest (runes)](#clnrest-runes) | [LND (REST)](#lnd-rest) | [OpenNode](#opennode) |
|
||||
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
||||
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
||||
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
||||
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
|
||||
| [Cliche Wallet](#cliche-wallet) | | |
|
||||
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
|
||||
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
||||
| [Fake Wallet](#fake-wallet) | | |
|
||||
|
||||
### LND (REST)
|
||||
---
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
|
||||
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
|
||||
- `LND_REST_CERT`: /file/path/tls.cert
|
||||
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
||||
<a id="clnrest-runes"></a>
|
||||
|
||||
or
|
||||
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
|
||||
|
||||
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
|
||||
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
|
||||
|
||||
### LND (gRPC)
|
||||
**Environment variables**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
|
||||
- `LND_GRPC_ENDPOINT`: ip_address
|
||||
- `LND_GRPC_PORT`: port
|
||||
- `LND_GRPC_CERT`: /file/path/tls.cert
|
||||
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `CLNRestWallet`
|
||||
- `CLNREST_URL`: `https://127.0.0.1:3010`
|
||||
- `CLNREST_CA`: `/home/lightningd/.lightning/bitcoin/ca.pem` (or the content of the file)
|
||||
- `CLNREST_CERT`: `/home/lightningd/.lightning/bitcoin/server.pem` (or the content of the file)
|
||||
- `CLNREST_LAST_PAY_INDEX`: `lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max'`
|
||||
- `CLNREST_NODEID`: `lightning-cli getinfo | jq -r .id` (only required for v23.08)
|
||||
|
||||
You can also use an AES-encrypted macaroon (more info) instead by using
|
||||
**Create runes (copy/paste)**
|
||||
|
||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||
```bash
|
||||
# Read-only: funds, pays, invoices, info, summary, and invoice listener
|
||||
lightning-cli createrune \
|
||||
restrictions='[["method=listfunds","method=listpays","method=listinvoices","method=getinfo","method=summary","method=waitanyinvoice"]]' \
|
||||
| jq -r .rune
|
||||
```
|
||||
|
||||
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
|
||||
```bash
|
||||
# Invoice: max 1,000,001 msat, label must start with "LNbits", 60 req/min
|
||||
lightning-cli createrune \
|
||||
restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' \
|
||||
| jq -r .rune
|
||||
```
|
||||
|
||||
### LNbits
|
||||
```bash
|
||||
# Pay: bolt11 amount < 1001 (msat), label must start with "LNbits", 1 req/min
|
||||
lightning-cli createrune \
|
||||
restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' \
|
||||
| jq -r .rune
|
||||
```
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
|
||||
- `LNBITS_ENDPOINT`: e.g. https://lnbits.com
|
||||
- `LNBITS_KEY`: lnbitsAdminKey
|
||||
```bash
|
||||
# Renepay: invstring amount < 1001 (msat), label must start with "LNbits", 1 req/min
|
||||
lightning-cli createrune \
|
||||
restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' \
|
||||
| jq -r .rune
|
||||
```
|
||||
|
||||
### LNPay
|
||||
Set the resulting values into:
|
||||
|
||||
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
|
||||
- `CLNREST_READONLY_RUNE`
|
||||
- `CLNREST_INVOICE_RUNE`
|
||||
- `CLNREST_PAY_RUNE`
|
||||
- `CLNREST_RENEPAY_RUNE`
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
|
||||
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
|
||||
- `LNPAY_API_KEY`: sak_apiKey
|
||||
- `LNPAY_WALLET_KEY`: waka_apiKey
|
||||
## CoreLightning
|
||||
|
||||
### OpenNode
|
||||
**Required env vars**
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `CoreLightningWallet`
|
||||
- `CORELIGHTNING_RPC`: `/file/path/lightning-rpc`
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||
## CoreLightning REST
|
||||
|
||||
### Alby
|
||||
Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-REST)
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **AlbyWallet**
|
||||
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
|
||||
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `CoreLightningRestWallet`
|
||||
- `CORELIGHTNING_REST_URL`: `http://127.0.0.1:8185/`
|
||||
- `CORELIGHTNING_REST_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
|
||||
- `CORELIGHTNING_REST_CERT`: `/home/lightning/clnrest/tls.cert`
|
||||
|
||||
### ZBD
|
||||
## Spark (Core Lightning)
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an ZBD API Key here: https://zbd.dev/docs/dashboard/projects/api
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
|
||||
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
|
||||
- `ZBD_API_KEY`: ZBDApiKey
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkWallet`
|
||||
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
|
||||
- `SPARK_TOKEN`: `secret_access_key`
|
||||
|
||||
### Cliche Wallet
|
||||
## Spark L2
|
||||
|
||||
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
|
||||
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
|
||||
|
||||
### Optional tuning
|
||||
|
||||
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
|
||||
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
|
||||
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
|
||||
|
||||
### Example: run the sidecar
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lnbits/spark_sidecar.git
|
||||
cd spark_sidecar
|
||||
npm install
|
||||
|
||||
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
|
||||
SPARK_NETWORK=MAINNET \
|
||||
SPARK_SIDECAR_PORT=8765 \
|
||||
SPARK_PAY_WAIT_MS=20000 \
|
||||
node server.mjs
|
||||
```
|
||||
|
||||
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
|
||||
|
||||
## LND (REST)
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `LndRestWallet`
|
||||
- `LND_REST_ENDPOINT`: `http://10.147.17.230:8080/`
|
||||
- `LND_REST_CERT`: `/file/path/tls.cert`
|
||||
- `LND_REST_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
|
||||
|
||||
or:
|
||||
|
||||
- `LND_REST_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
|
||||
|
||||
## LND (gRPC)
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `LndWallet`
|
||||
- `LND_GRPC_ENDPOINT`: `ip_address`
|
||||
- `LND_GRPC_PORT`: `port`
|
||||
- `LND_GRPC_CERT`: `/file/path/tls.cert`
|
||||
- `LND_GRPC_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
|
||||
|
||||
You can also use an AES-encrypted macaroon instead:
|
||||
|
||||
- `LND_GRPC_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
|
||||
|
||||
To encrypt your macaroon:
|
||||
|
||||
```bash
|
||||
uv run lnbits-cli encrypt macaroon
|
||||
```
|
||||
|
||||
## LNbits
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `LNbitsWallet`
|
||||
- `LNBITS_ENDPOINT`: for example `https://lnbits.com`
|
||||
- `LNBITS_KEY`: `lnbitsAdminKey`
|
||||
|
||||
## LNPay
|
||||
|
||||
For the invoice listener to work you must have a publicly accessible URL in your LNbits and set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the event **Wallet Receive** and no secret. Example: [https://mylnbits/wallet/webhook](`https://mylnbits/wallet/webhook).
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `LNPayWallet`
|
||||
- `LNPAY_API_ENDPOINT`: `https://api.lnpay.co/v1/`
|
||||
- `LNPAY_API_KEY`: `sak_apiKey`
|
||||
- `LNPAY_WALLET_KEY`: `waka_apiKey`
|
||||
|
||||
## OpenNode
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `OpenNodeWallet`
|
||||
- `OPENNODE_API_ENDPOINT`: `https://api.opennode.com/`
|
||||
- `OPENNODE_KEY`: `opennodeAdminApiKey`
|
||||
|
||||
## Blink
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
|
||||
|
||||
You can generate a Blink API key at [https://dashboard.blink.sv](https://dashboard.blink.sv). More info: [https://dev.blink.sv/api/auth#create-an-api-key](https://dev.blink.sv/api/auth#create-an-api-key)
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `BlinkWallet`
|
||||
- `BLINK_API_ENDPOINT`: `https://api.blink.sv/graphql`
|
||||
- `BLINK_WS_ENDPOINT`: `wss://ws.blink.sv/graphql`
|
||||
- `BLINK_TOKEN`: `BlinkToken`
|
||||
|
||||
## Alby
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
|
||||
|
||||
Generate an Alby access token here: [https://getalby.com/developer/access_tokens/new](https://getalby.com/developer/access_tokens/new)
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `AlbyWallet`
|
||||
- `ALBY_API_ENDPOINT`: `https://api.getalby.com/`
|
||||
- `ALBY_ACCESS_TOKEN`: `AlbyAccessToken`
|
||||
|
||||
## Boltz
|
||||
|
||||
This connects to a running [boltz-client](https://docs.boltz.exchange/v/boltz-client) and handles Lightning payments through submarine swaps on the Liquid network.
|
||||
|
||||
You can run the daemon in standalone mode via `standalone = True` in the config or `boltzd --standalone`. Create a Liquid wallet with:
|
||||
|
||||
```bash
|
||||
boltzcli wallet create lnbits lbtc
|
||||
```
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `BoltzWallet`
|
||||
- `BOLTZ_CLIENT_ENDPOINT`: `127.0.0.1:9002`
|
||||
- `BOLTZ_CLIENT_MACAROON`: `/home/bob/.boltz/macaroons/admin.macaroon` or Base64/Hex
|
||||
- `BOLTZ_CLIENT_CERT`: `/home/bob/.boltz/tls.cert` or Base64/Hex
|
||||
- `BOLTZ_CLIENT_WALLET`: `lnbits`
|
||||
|
||||
## ZBD
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
|
||||
|
||||
Generate a ZBD API key here: [https://zbd.dev/docs/dashboard/projects/api](https://zbd.dev/docs/dashboard/projects/api)
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `ZBDWallet`
|
||||
- `ZBD_API_ENDPOINT`: `https://api.zebedee.io/v0/`
|
||||
- `ZBD_API_KEY`: `ZBDApiKey`
|
||||
|
||||
## Phoenixd
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||
|
||||
You can get a phoenixd API key from `~/.phoenix/phoenix.conf`. See the phoenixd documentation for details.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `PhoenixdWallet`
|
||||
- `PHOENIXD_API_ENDPOINT`: `http://localhost:9740/`
|
||||
- `PHOENIXD_API_PASSWORD`: `PhoenixdApiPassword`
|
||||
|
||||
## Eclair (ACINQ)
|
||||
|
||||
<a id="eclair-acinq"></a>
|
||||
|
||||
Connect to an existing Eclair node so your backend handles invoices and payments.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `EclairWallet`
|
||||
- `ECLAIR_URL`: `http://127.0.0.1:8283`
|
||||
- `ECLAIR_PASSWORD`: `eclairpw`
|
||||
|
||||
## Fake Wallet
|
||||
|
||||
<a id="fake-wallet"></a>
|
||||
|
||||
A testing-only backend that mints accounting units inside LNbits accounting (no real sats).
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `FakeWallet`
|
||||
- `FAKE_SECRET`: `ToTheMoon1`
|
||||
- `FAKE_UNIT`: `sats`
|
||||
|
||||
## LN.tips
|
||||
|
||||
<a id="lntips"></a>
|
||||
|
||||
As the initinal LN.tips bot is no longer active the code still exists and is widly used. Connect one of custodial services as your backend to create and pay Lightning invoices through their API or selfhost this service and run it as funding source.
|
||||
Resources: https://github.com/massmux/SatsMobiBot
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `LNTipsWallet`
|
||||
- `LNTIPS_API_ENDPOINT`: `https://ln.tips`
|
||||
- `LNTIPS_API_KEY`: `LNTIPS_ADMIN_KEY`
|
||||
|
||||
## Breez SDK
|
||||
|
||||
A Greenlight invite code or Greenlight partner certificate/key can register a new node with Greenlight. If the Greenlight node already exists, neither is required.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `BreezSdkWallet`
|
||||
- `BREEZ_API_KEY`: `...`
|
||||
- `BREEZ_GREENLIGHT_SEED`: `...`
|
||||
- `BREEZ_GREENLIGHT_INVITE_CODE`: `...`
|
||||
- `BREEZ_GREENLIGHT_DEVICE_KEY`: `/path/to/breezsdk/device.pem` or Base64/Hex
|
||||
- `BREEZ_GREENLIGHT_DEVICE_CERT`: `/path/to/breezsdk/device.crt` or Base64/Hex
|
||||
|
||||
## Breez Liquid SDK
|
||||
|
||||
This uses the [Breez SDK - Liquid](https://sdk-doc-liquid.breez.technology/) to manage Lightning payments via submarine swaps on the Liquid network. Provide a mnemonic seed phrase (for example, generate one with a Liquid wallet like [Blockstream Green](https://blockstream.com/green/)) and set it in the environment or admin UI.
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `BreezLiquidSdkWallet`
|
||||
- `BREEZ_LIQUID_SEED`: `...`
|
||||
|
||||
Fees apply for each submarine swap. You may need to increase the reserve fee under **Settings → Funding** or via:
|
||||
|
||||
- `LNBITS_RESERVE_FEE_MIN`: `...`
|
||||
- `LNBITS_RESERVE_FEE_PERCENT`: `...`
|
||||
|
||||
## Cliche Wallet
|
||||
|
||||
**Required env vars**
|
||||
|
||||
- `CLICHE_ENDPOINT`: `ws://127.0.0.1:12000`
|
||||
|
||||
## Nostr Wallet Connect (NWC)
|
||||
|
||||
To use NWC as a funding source you need a pairing URL (pairing secret) from an NWC provider. See providers here:
|
||||
[https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets)
|
||||
|
||||
Configure in the admin UI or via env vars:
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: `NWCWallet`
|
||||
- `NWC_PAIRING_URL`: `nostr+walletconnect://...your...pairing...secret...`
|
||||
|
||||
<a id="strike"></a>
|
||||
|
||||
## Strike (alpha)
|
||||
|
||||
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
||||
|
||||
Get more info here [https://docs.strike.me/strike-oauth-connect/](https://docs.strike.me/strike-oauth-connect/)
|
||||
|
||||
**Integration endpoints**
|
||||
|
||||
- `STRIKE_API_ENDPOINT`: `https://api.strike.me/v1`
|
||||
- `STRIKE_API_KEY`: `YOUR_STRIKE_API_KEY`
|
||||
|
||||
---
|
||||
|
||||
## Additional Guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||
- **[User Roles](./user_roles.md)** — Quick Overview of existing Roles in LNBits.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
|
||||
@@ -4,21 +4,17 @@ title: User’s Guide
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
|
||||
LNbits, free and open-source lightning-network wallet/accounts system
|
||||
=====================================================================
|
||||
# LNbits, free and open-source Lightning Network wallet/accounts system
|
||||
|
||||
LNbits is a very simple Python application that sits on top of any funding source, and can be used as:
|
||||
|
||||
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
|
||||
* Part of a development stack via LNbits API
|
||||
* Fallback wallet for the LNURL scheme
|
||||
* Instant wallet for LN demonstrations
|
||||
- Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
|
||||
- Extendable platform for exploring Lightning Network functionality via LNbits extension framework
|
||||
- Part of a development stack via LNbits API
|
||||
- Fallback wallet for the LNURL scheme
|
||||
- Instant wallet for LN demonstrations
|
||||
|
||||
|
||||
LNbits as an account system
|
||||
---------------------------
|
||||
## LNbits as an account system
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending,
|
||||
export to csv + more to come...
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
{
|
||||
"nodes": {
|
||||
"build-system-pkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"pyproject-nix"
|
||||
],
|
||||
"uv2nix": "uv2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755484659,
|
||||
"narHash": "sha256-2FfbqsaHVQd12XFFUAinIMAuGO3853LONmva1gT3vKw=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "build-system-pkgs",
|
||||
"rev": "9778e87c2361810ff15e287ca5895c9da4a0e900",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "build-system-pkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,71 +42,49 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698974481,
|
||||
"narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1702233072,
|
||||
"narHash": "sha256-H5G2wgbim2Ku6G6w+NSaQaauv6B6DlPhY9fMvArKqRo=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1751274312,
|
||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "781e2a9797ecf0f146e81425c822dca69fe4a348",
|
||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-23.11",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"pyproject-nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1702334837,
|
||||
"narHash": "sha256-QZG6+zFshyY+L8m2tlOTm75U5m9y7z01g0josVK+8Os=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "1f4bcbf1be73abc232a972a77102a3e820485a99",
|
||||
"lastModified": 1754923840,
|
||||
"narHash": "sha256-QSKpYg+Ts9HYF155ltlj40iBex39c05cpOF8gjoE2EM=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"rev": "023cd4be230eacae52635be09eef100c37ef78da",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"build-system-pkgs": "build-system-pkgs",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
"pyproject-nix": "pyproject-nix",
|
||||
"uv2nix": "uv2nix_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -100,38 +102,51 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"uv2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"build-system-pkgs",
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"build-system-pkgs",
|
||||
"pyproject-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1699786194,
|
||||
"narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
|
||||
"lastModified": 1755210905,
|
||||
"narHash": "sha256-WnoFEk79ysjL85TNP7bvImzhxvQw9B6uNtnLd4oJntw=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "87bcba013ef304bbfd67c8e8a257aee634ed5a4c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"uv2nix_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"pyproject-nix": [
|
||||
"pyproject-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755485731,
|
||||
"narHash": "sha256-k8kxwVs8Oze6q/jAaRa3RvZbb50I/K0b5uptlsh0HXI=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"rev": "bebbd80bf56110fcd20b425589814af28f1939eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "uv2nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,151 @@
|
||||
{
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system";
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system (uv2nix)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
|
||||
uv2nix.url = "github:pyproject-nix/uv2nix";
|
||||
build-system-pkgs.url = "github:pyproject-nix/build-system-pkgs";
|
||||
|
||||
pyproject-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
uv2nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
build-system-pkgs.inputs.nixpkgs.follows = "nixpkgs";
|
||||
uv2nix.inputs.pyproject-nix.follows = "pyproject-nix";
|
||||
build-system-pkgs.inputs.pyproject-nix.follows = "pyproject-nix";
|
||||
};
|
||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forSystems = systems: f:
|
||||
nixpkgs.lib.genAttrs systems
|
||||
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
|
||||
forAllSystems = forSystems supportedSystems;
|
||||
projectName = "lnbits";
|
||||
in
|
||||
{
|
||||
overlays = {
|
||||
default = final: prev: {
|
||||
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
|
||||
};
|
||||
};
|
||||
packages = forAllSystems (system: pkgs: {
|
||||
default = self.packages.${system}.${projectName};
|
||||
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
|
||||
projectDir = ./.;
|
||||
meta.rev = self.dirtyRev or self.rev;
|
||||
meta.mainProgram = projectName;
|
||||
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
|
||||
protobuf = prev.protobuf.override { preferWheel = true; };
|
||||
ruff = prev.ruff.override { preferWheel = true; };
|
||||
wallycore = prev.wallycore.override { preferWheel = true; };
|
||||
});
|
||||
};
|
||||
});
|
||||
nixosModules = {
|
||||
default = { pkgs, lib, config, ... }: {
|
||||
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
|
||||
nixpkgs.overlays = [ self.overlays.default ];
|
||||
};
|
||||
};
|
||||
checks = forAllSystems (system: pkgs:
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, uv2nix, pyproject-nix, build-system-pkgs, ... }:
|
||||
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]
|
||||
(system:
|
||||
let
|
||||
vmTests = import ./nix/tests {
|
||||
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
|
||||
inherit inputs pkgs;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
lib = pkgs.lib;
|
||||
|
||||
python = pkgs.python312;
|
||||
|
||||
# Read uv.lock / pyproject via uv2nix
|
||||
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
|
||||
|
||||
# Prefer wheels when available
|
||||
uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
|
||||
|
||||
# Helper for extending lists safely (works if a is null)
|
||||
plus = a: b: lib.unique (((if a == null then [] else a)) ++ b);
|
||||
|
||||
# Extra build inputs for troublesome sdists
|
||||
myOverrides = (final: prev: {
|
||||
# embit needs setuptools at build time
|
||||
embit = prev.embit.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# http-ece (pywebpush dep) needs setuptools
|
||||
"http-ece" = prev."http-ece".overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# pyqrcode needs setuptools
|
||||
pyqrcode = prev.pyqrcode.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# tlv8 needs setuptools
|
||||
tlv8 = prev.tlv8.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
|
||||
});
|
||||
|
||||
# secp256k1 Python binding:
|
||||
# - setuptools, pkg-config
|
||||
# - cffi + pycparser
|
||||
# - system libsecp256k1 for headers/libs
|
||||
secp256k1 = prev.secp256k1.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [
|
||||
prev.setuptools
|
||||
pkgs.pkg-config
|
||||
prev.cffi
|
||||
prev.pycparser
|
||||
];
|
||||
buildInputs = plus (old.buildInputs or []) [ pkgs.secp256k1 ];
|
||||
propagatedBuildInputs = plus (old.propagatedBuildInputs or []) [ prev.cffi prev.pycparser ];
|
||||
env = (old.env or { }) // { PKG_CONFIG = "${pkgs.pkg-config}/bin/pkg-config"; };
|
||||
});
|
||||
|
||||
# pynostr uses setuptools-scm for versioning
|
||||
pynostr = prev.pynostr.overrideAttrs (old: {
|
||||
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools-scm ];
|
||||
});
|
||||
});
|
||||
|
||||
# Compose Python package set honoring uv.lock
|
||||
pythonSet =
|
||||
(pkgs.callPackage pyproject-nix.build.packages { inherit python; })
|
||||
.overrideScope (lib.composeManyExtensions [
|
||||
build-system-pkgs.overlays.default
|
||||
uvLockedOverlay
|
||||
myOverrides
|
||||
]);
|
||||
|
||||
projectName = "lnbits";
|
||||
|
||||
# Build a venv from the locked spec (this installs the resolved wheels)
|
||||
runtimeVenv = pythonSet.mkVirtualEnv "${projectName}-env" workspace.deps.default;
|
||||
|
||||
# Wrapper so `nix run` behaves like `uv run` (use local source tree for templates/static/extensions)
|
||||
lnbitsApp = pkgs.writeShellApplication {
|
||||
name = "lnbits";
|
||||
text = ''
|
||||
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||
export REQUESTS_CA_BUNDLE=$SSL_CERT_FILE
|
||||
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
|
||||
exec ${runtimeVenv}/bin/lnbits "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
lnbitsCliApp = pkgs.writeShellApplication {
|
||||
name = "lnbits-cli";
|
||||
text = ''
|
||||
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
|
||||
exec ${runtimeVenv}/bin/lnbits-cli "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
|
||||
//
|
||||
{
|
||||
# Other checks here...
|
||||
}
|
||||
);
|
||||
};
|
||||
# nix build → produces the venv in ./result
|
||||
packages.default = runtimeVenv;
|
||||
packages.${projectName} = runtimeVenv;
|
||||
|
||||
# nix run . → launches via wrapper that imports from source tree
|
||||
apps.default = { type = "app"; program = "${lnbitsApp}/bin/lnbits"; };
|
||||
apps.${projectName} = self.apps.${system}.default;
|
||||
apps."${projectName}-cli" = { type = "app"; program = "${lnbitsCliApp}/bin/lnbits-cli"; };
|
||||
|
||||
# dev shell with locked deps + tools
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
runtimeVenv
|
||||
pkgs.uv
|
||||
pkgs.ruff
|
||||
pkgs.black
|
||||
pkgs.mypy
|
||||
pkgs.pre-commit
|
||||
pkgs.openapi-generator-cli
|
||||
];
|
||||
};
|
||||
|
||||
overlays.default = final: prev: {
|
||||
${projectName} = self.packages.${final.stdenv.hostPlatform.system}.${projectName};
|
||||
replaceVars = prev.replaceVars or (path: vars: prev.substituteAll ({ src = path; } // vars));
|
||||
};
|
||||
|
||||
# System-specific nixos modules to avoid circular dependency
|
||||
nixosModules.default = { pkgs, lib, config, ... }: {
|
||||
imports = [ "${./nix/modules/lnbits-service.nix}" ];
|
||||
nixpkgs.overlays = [ self.overlays.${system}.default ];
|
||||
};
|
||||
|
||||
checks =
|
||||
import ./nix/tests { inherit pkgs; flake = self; };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,488 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Account" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get main page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Create new account" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"name":"a1"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/account</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract user and wallet" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
vars.put("userId", resp.user || "no-user-id");
|
||||
vars.put("walletId", resp.id || "no-wallet-id");
|
||||
vars.put("inkey", resp.inkey || 'no-inkey');
|
||||
vars.put("adminkey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Login with user id" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"usr":"${userId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/usr</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Go to wallet page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="wal" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.name">wal</stringProp>
|
||||
<stringProp name="Argument.value">${walletId}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get currencies" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/currencies</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">undefined</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Currency List" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="1438956759">["AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP","BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNH","CNY","COP","CRC","CUC","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD","FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRT","ISK","JEP","JMD","JOD","JPY","KES","KGS","KHR","KMF","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LYD","MAD","MDL","MGA","MKD","MMK","MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SEK","SGD","SHP","SLL","SOS","SRD","SSP","STD","SVC","SZL","THB","TJS","TMT","TND","TOP","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VES","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPD","XPF","XPT","YER","ZAR","ZMW","ZWL"]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get wallet balance" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallet</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Check Balance is Zero" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
|
||||
|
||||
if (resp.balance !== 0) {
|
||||
AssertionResult.setFailureMessage("Balance is not zero, but: "+ resp.balance);
|
||||
AssertionResult.setFailure(true)
|
||||
}
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223Assertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get payments" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/payments</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="X-Api-Key" elementType="Header">
|
||||
<stringProp name="Header.name">X-Api-Key</stringProp>
|
||||
<stringProp name="Header.value">${inkey}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check empty payment list" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="2914">[]</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
@@ -1,487 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
|
||||
<hashTree>
|
||||
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
|
||||
<hashTree>
|
||||
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Server" enabled="true"/>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to home page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
<stringProp name="TestPlan.comments">Detected the start of a redirect chain</stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
<stringProp name="50554">307</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">40</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract `is_first_install` var" enabled="true">
|
||||
<stringProp name="RegexExtractor.useHeaders">true</stringProp>
|
||||
<stringProp name="RegexExtractor.refname">is_first_install</stringProp>
|
||||
<stringProp name="RegexExtractor.regex">location: (.*)</stringProp>
|
||||
<stringProp name="RegexExtractor.template">$1$</stringProp>
|
||||
<stringProp name="RegexExtractor.default">not-first-install</stringProp>
|
||||
<stringProp name="RegexExtractor.match_number">0</stringProp>
|
||||
<stringProp name="Sample.scope">all</stringProp>
|
||||
</RegexExtractor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="If First Install" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to first_install" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Init super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password","password_repeat":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/first_install</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/first_install</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="Else (not first install)" enabled="true">
|
||||
<stringProp name="IfController.condition">${__groovy(!vars.get('is_first_install').contains("first_install"),)}</stringProp>
|
||||
<boolProp name="IfController.evaluateAll">false</boolProp>
|
||||
<boolProp name="IfController.useExpression">true</boolProp>
|
||||
</IfController>
|
||||
<hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Login super user" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"username":"admin","password":"admin_password"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Get all admin wallets" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/wallets</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
|
||||
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
|
||||
<stringProp name="Header.value">1</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract admin wallet and keys" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())[0]
|
||||
|
||||
vars.put("adminWalletId", resp.id || "no-admin-wallet-id");
|
||||
vars.put("adminWalletKey", resp.adminkey || 'no-adminkey');
|
||||
</stringProp>
|
||||
<stringProp name="scriptLanguage">javascript</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Top up wallet" enabled="true">
|
||||
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">{"amount":"1000000","id":"${adminWalletId}"}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/admin/api/v1/topup/</stringProp>
|
||||
<stringProp name="HTTPSampler.method">PUT</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?&wal=c1daa33fc4014ef69cd1505f14f322c2</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Logout super user" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${host}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${port}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
<stringProp name="HTTPSampler.path">/api/v1/auth/logout</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Referer" elementType="Header">
|
||||
<stringProp name="Header.name">Referer</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Language" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Language</stringProp>
|
||||
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Origin" elementType="Header">
|
||||
<stringProp name="Header.name">Origin</stringProp>
|
||||
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/json</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept-Encoding" elementType="Header">
|
||||
<stringProp name="Header.name">Accept-Encoding</stringProp>
|
||||
<stringProp name="Header.value">gzip, deflate</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="User-Agent" elementType="Header">
|
||||
<stringProp name="Header.name">User-Agent</stringProp>
|
||||
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="49586">200</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.custom_message"></stringProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">8</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config you might tweak ---
|
||||
REPO_URL="https://github.com/lnbits/lnbits.git"
|
||||
BRANCH="main"
|
||||
APP_DIR="${PWD}/lnbits"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
PORT="${PORT:-5000}"
|
||||
ADMIN_UI="${LNBITS_ADMIN_UI:-true}"
|
||||
# -------------------------------
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Ensure basic tooling
|
||||
if ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl git
|
||||
fi
|
||||
|
||||
# System build deps and secp headers
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
pkg-config \
|
||||
build-essential \
|
||||
libsecp256k1-dev \
|
||||
automake \
|
||||
autoconf \
|
||||
libtool \
|
||||
m4
|
||||
fi
|
||||
|
||||
# Install uv (if missing)
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
# Ensure PATH for current session
|
||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
|
||||
# Clone or reuse repo
|
||||
if [[ ! -d "$APP_DIR/.git" ]]; then
|
||||
git clone "$REPO_URL" "$APP_DIR"
|
||||
fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
git fetch --all --prune
|
||||
git checkout "$BRANCH"
|
||||
git pull --ff-only || true
|
||||
|
||||
# First-run setup
|
||||
mkdir -p data
|
||||
[[ -f .env ]] || cp .env.example .env || true
|
||||
|
||||
# Prefer system libsecp256k1 (avoid autotools path)
|
||||
export SECP_BUNDLED=0
|
||||
|
||||
# Sync dependencies with Python 3.12
|
||||
uv sync --python 3.12 --all-extras --no-dev
|
||||
|
||||
# Environment
|
||||
export LNBITS_ADMIN_UI="$ADMIN_UI"
|
||||
export HOST="$HOST"
|
||||
export PORT="$PORT"
|
||||
|
||||
# Open firewall (optional)
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
sudo ufw allow "$PORT"/tcp || true
|
||||
fi
|
||||
|
||||
# Run LNbits with Python 3.12 via uv
|
||||
exec uv run --python 3.12 lnbits
|
||||
@@ -0,0 +1,23 @@
|
||||
from .core.services import create_invoice, pay_invoice
|
||||
from .decorators import (
|
||||
check_account_exists,
|
||||
check_admin,
|
||||
check_super_user,
|
||||
check_user_exists,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from .exceptions import InvoiceError, PaymentError
|
||||
|
||||
__all__ = [
|
||||
"InvoiceError",
|
||||
"PaymentError",
|
||||
"check_account_exists",
|
||||
"check_admin",
|
||||
"check_super_user",
|
||||
"check_user_exists",
|
||||
"create_invoice",
|
||||
"pay_invoice",
|
||||
"require_admin_key",
|
||||
"require_invoice_key",
|
||||
]
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
killswitch_task,
|
||||
wait_for_paid_invoices,
|
||||
from lnbits.core.crud import (
|
||||
get_db_version,
|
||||
get_installed_extensions,
|
||||
update_installed_extension_state,
|
||||
)
|
||||
from lnbits.core.crud.extensions import create_installed_extension
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||
from lnbits.core.services.notifications import enqueue_admin_notification
|
||||
from lnbits.core.services.payments import check_pending_payments
|
||||
from lnbits.core.tasks import (
|
||||
audit_queue,
|
||||
collect_exchange_rates_data,
|
||||
purge_audit_data,
|
||||
run_by_the_minute_tasks,
|
||||
wait_for_audit_data,
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.helpers import version_parse
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import (
|
||||
cancel_all_tasks,
|
||||
@@ -37,35 +47,100 @@ from lnbits.tasks import (
|
||||
register_invoice_listener,
|
||||
)
|
||||
from lnbits.utils.cache import cache
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
from lnbits.utils.logger import (
|
||||
configure_logger,
|
||||
initialize_server_websocket_logger,
|
||||
log_server_info,
|
||||
)
|
||||
from lnbits.wallets import get_funding_source, set_funding_source
|
||||
|
||||
from .commands import migrate_databases
|
||||
from .core import init_core_routers
|
||||
from .core.db import core_app_extra
|
||||
from .core.models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||
from .core.services import check_admin_settings, check_webpush_settings
|
||||
from .core.views.api import add_installed_extension
|
||||
from .core.views.generic import update_installed_extension_state
|
||||
from .extension_manager import (
|
||||
Extension,
|
||||
InstallableExtension,
|
||||
get_valid_extensions,
|
||||
version_parse,
|
||||
)
|
||||
from .helpers import template_renderer
|
||||
from .middleware import (
|
||||
CustomGZipMiddleware,
|
||||
AuditMiddleware,
|
||||
ExtensionsRedirectMiddleware,
|
||||
InstalledExtensionMiddleware,
|
||||
add_first_install_middleware,
|
||||
add_ip_block_middleware,
|
||||
add_ratelimit_middleware,
|
||||
)
|
||||
from .requestvars import g
|
||||
from .tasks import (
|
||||
check_pending_payments,
|
||||
internal_invoice_listener,
|
||||
invoice_listener,
|
||||
)
|
||||
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
||||
|
||||
|
||||
async def startup(app: FastAPI):
|
||||
logger.info(f"Starting LNbits Version: {settings.version}")
|
||||
start = time.perf_counter()
|
||||
settings.lnbits_running = True
|
||||
# wait till migration is done
|
||||
await migrate_databases()
|
||||
|
||||
# setup admin settings
|
||||
await check_admin_settings()
|
||||
core_app_extra.register_new_ratelimiter()
|
||||
await check_webpush_settings()
|
||||
|
||||
# check extensions after restart
|
||||
if not settings.lnbits_extensions_deactivate_all:
|
||||
await check_and_register_extensions(app)
|
||||
|
||||
log_server_info()
|
||||
|
||||
# initialize WALLET
|
||||
try:
|
||||
set_funding_source()
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing {settings.lnbits_backend_wallet_class}: {e}")
|
||||
set_void_wallet_class()
|
||||
|
||||
# initialize funding source
|
||||
await check_funding_source()
|
||||
|
||||
# register core routes
|
||||
init_core_routers(app)
|
||||
|
||||
# initialize tasks
|
||||
register_async_tasks()
|
||||
|
||||
enqueue_admin_notification(
|
||||
NotificationType.server_start_stop,
|
||||
{
|
||||
"message": "LNbits server started.",
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
},
|
||||
)
|
||||
|
||||
end = time.perf_counter()
|
||||
logger.success(f"LNbits started in {end - start:.2f} seconds.")
|
||||
|
||||
|
||||
async def shutdown():
|
||||
logger.warning("LNbits shutting down...")
|
||||
enqueue_admin_notification(
|
||||
NotificationType.server_start_stop,
|
||||
{
|
||||
"message": "LNbits server shutting down...",
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
},
|
||||
)
|
||||
settings.lnbits_running = False
|
||||
|
||||
# shutdown event
|
||||
cancel_all_tasks()
|
||||
|
||||
# wait a bit to allow them to finish, so that cleanup can run without problems
|
||||
await asyncio.sleep(0.1)
|
||||
funding_source = get_funding_source()
|
||||
await funding_source.cleanup()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await startup(app)
|
||||
yield
|
||||
await shutdown()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@@ -77,6 +152,7 @@ def create_app() -> FastAPI:
|
||||
"accounts system with plugins."
|
||||
),
|
||||
version=settings.version,
|
||||
lifespan=lifespan,
|
||||
license_info={
|
||||
"name": "MIT License",
|
||||
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
|
||||
@@ -84,23 +160,24 @@ def create_app() -> FastAPI:
|
||||
)
|
||||
|
||||
# Allow registering new extensions routes without direct access to the `app` object
|
||||
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
|
||||
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
|
||||
core_app_extra.register_new_ext_routes = register_new_ext_routes(app)
|
||||
core_app_extra.register_new_ratelimiter = register_new_ratelimiter(app)
|
||||
|
||||
# register static files
|
||||
static_path = Path("lnbits", "static")
|
||||
static = StaticFiles(directory=static_path)
|
||||
app.mount("/static", static, name="static")
|
||||
|
||||
g().base_url = f"http://{settings.host}:{settings.port}"
|
||||
app.mount("/static", StaticFiles(directory=Path("lnbits", "static")), name="static")
|
||||
Path(settings.lnbits_data_folder, "images").mkdir(parents=True, exist_ok=True)
|
||||
app.mount(
|
||||
"/library",
|
||||
StaticFiles(directory=Path(settings.lnbits_data_folder, "images")),
|
||||
name="library",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||
)
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
app.add_middleware(
|
||||
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
|
||||
)
|
||||
app.add_middleware(AuditMiddleware, audit_queue=audit_queue)
|
||||
|
||||
# required for SSO login
|
||||
app.add_middleware(SessionMiddleware, secret_key=settings.auth_secret_key)
|
||||
@@ -117,65 +194,50 @@ def create_app() -> FastAPI:
|
||||
add_ip_block_middleware(app)
|
||||
add_ratelimit_middleware(app)
|
||||
|
||||
register_startup(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
register_shutdown(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
async def check_funding_source() -> None:
|
||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||
funding_source = get_funding_source()
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
logger.debug(
|
||||
f"SIGINT received, terminating LNbits. signal: {signal}, frame: {frame}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
WALLET = get_wallet_class()
|
||||
|
||||
# fallback to void after 30 seconds of failures
|
||||
sleep_time = 5
|
||||
timeout = int(30 / sleep_time)
|
||||
|
||||
balance = 0
|
||||
max_retries = settings.funding_source_max_retries
|
||||
retry_counter = 0
|
||||
|
||||
while True:
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
error_message, balance = await WALLET.status()
|
||||
logger.info(f"Connecting to backend {funding_source.__class__.__name__}...")
|
||||
error_message, balance = await funding_source.status()
|
||||
if not error_message:
|
||||
retry_counter = 0
|
||||
logger.success(
|
||||
f"✔️ Backend {funding_source.__class__.__name__} connected "
|
||||
f"and with a balance of {balance} msat."
|
||||
)
|
||||
break
|
||||
|
||||
logger.error(
|
||||
f"The backend for {WALLET.__class__.__name__} isn't "
|
||||
f"The backend for {funding_source.__class__.__name__} isn't "
|
||||
f"working properly: '{error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to {WALLET.__class__.__name__}: {e}")
|
||||
pass
|
||||
logger.error(
|
||||
f"Error connecting to {funding_source.__class__.__name__}: {e}"
|
||||
)
|
||||
|
||||
if settings.lnbits_admin_ui and retry_counter == timeout:
|
||||
if retry_counter >= max_retries:
|
||||
set_void_wallet_class()
|
||||
WALLET = get_wallet_class()
|
||||
funding_source = get_funding_source()
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Retrying connection to backend in {sleep_time} seconds...")
|
||||
retry_counter += 1
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
signal.signal(signal.SIGINT, original_sigint_handler)
|
||||
|
||||
logger.success(
|
||||
f"✔️ Backend {WALLET.__class__.__name__} connected "
|
||||
f"and with a balance of {balance} msat."
|
||||
)
|
||||
retry_counter += 1
|
||||
sleep_time = min(0.25 * (2**retry_counter), 60)
|
||||
logger.warning(
|
||||
f"Retrying connection to backend in {sleep_time} seconds... "
|
||||
f"({retry_counter}/{max_retries})"
|
||||
)
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
|
||||
def set_void_wallet_class():
|
||||
@@ -183,7 +245,7 @@ def set_void_wallet_class():
|
||||
"Fallback to VoidWallet, because the backend for "
|
||||
f"{settings.lnbits_backend_wallet_class} isn't working properly"
|
||||
)
|
||||
set_wallet_class("VoidWallet")
|
||||
set_funding_source("VoidWallet")
|
||||
|
||||
|
||||
async def check_installed_extensions(app: FastAPI):
|
||||
@@ -193,7 +255,7 @@ async def check_installed_extensions(app: FastAPI):
|
||||
re-created. The 'data' directory (where the '.zip' files live) is expected to
|
||||
persist state. Zips that are missing will be re-downloaded.
|
||||
"""
|
||||
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
|
||||
|
||||
installed_extensions = await build_all_installed_extensions_list(False)
|
||||
|
||||
for ext in installed_extensions:
|
||||
@@ -207,6 +269,7 @@ async def check_installed_extensions(app: FastAPI):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
await deactivate_extension(ext.id)
|
||||
logger.warning(
|
||||
f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
|
||||
)
|
||||
@@ -216,18 +279,37 @@ async def check_installed_extensions(app: FastAPI):
|
||||
logger.info(f"{ext.id} ({ext.installed_version})")
|
||||
|
||||
|
||||
async def build_all_installed_extensions_list(
|
||||
include_deactivated: Optional[bool] = True,
|
||||
) -> List[InstallableExtension]:
|
||||
async def build_all_installed_extensions_list( # noqa: C901
|
||||
include_deactivated: bool | None = True,
|
||||
) -> list[InstallableExtension]:
|
||||
"""
|
||||
Returns a list of all the installed extensions plus the extensions that
|
||||
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
||||
"""
|
||||
installed_extensions = await get_installed_extensions()
|
||||
settings.lnbits_installed_extensions_ids = {e.id for e in installed_extensions}
|
||||
|
||||
for ext_dir in Path(settings.lnbits_extensions_path, "extensions").iterdir():
|
||||
try:
|
||||
if not ext_dir.is_dir():
|
||||
continue
|
||||
ext_id = ext_dir.name
|
||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
||||
continue
|
||||
ext_info = InstallableExtension.from_ext_dir(ext_id)
|
||||
if not ext_info:
|
||||
continue
|
||||
|
||||
installed_extensions.append(ext_info)
|
||||
await create_installed_extension(ext_info)
|
||||
current_version = await get_db_version(ext_id)
|
||||
await migrate_extension_database(ext_info, current_version)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
installed_extensions_ids = [e.id for e in installed_extensions]
|
||||
for ext_id in settings.lnbits_extensions_default_install:
|
||||
if ext_id in installed_extensions_ids:
|
||||
if ext_id in settings.lnbits_installed_extensions_ids:
|
||||
continue
|
||||
|
||||
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
@@ -238,8 +320,13 @@ async def build_all_installed_extensions_list(
|
||||
release = next((e for e in ext_releases if e.is_version_compatible), None)
|
||||
|
||||
if release:
|
||||
ext_meta = ExtensionMeta(installed_release=release)
|
||||
ext_info = InstallableExtension(
|
||||
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
|
||||
id=ext_id,
|
||||
name=ext_id,
|
||||
version=release.version,
|
||||
icon=release.icon,
|
||||
meta=ext_meta,
|
||||
)
|
||||
installed_extensions.append(ext_info)
|
||||
|
||||
@@ -262,7 +349,7 @@ async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
|
||||
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
|
||||
|
||||
if f"./{str(ext.zip_path)}" not in zip_files:
|
||||
if f"./{ext.zip_path!s}" not in zip_files:
|
||||
await ext.download_archive()
|
||||
ext.extract_archive()
|
||||
|
||||
@@ -270,33 +357,24 @@ async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
|
||||
|
||||
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
||||
await add_installed_extension(ext)
|
||||
await update_installed_extension_state(ext_id=ext.id, active=True)
|
||||
|
||||
extension = Extension.from_installable_ext(ext)
|
||||
register_ext_routes(app, extension)
|
||||
|
||||
current_version = (await get_dbversions()).get(ext.id, 0)
|
||||
await migrate_extension_database(extension, current_version)
|
||||
current_version = await get_db_version(ext.id)
|
||||
await migrate_extension_database(ext, current_version)
|
||||
|
||||
# mount routes for the new version
|
||||
core_app_extra.register_new_ext_routes(extension)
|
||||
if extension.upgrade_hash:
|
||||
ext.nofiy_upgrade()
|
||||
|
||||
|
||||
def register_routes(app: FastAPI) -> None:
|
||||
"""Register FastAPI routes / LNbits extensions."""
|
||||
init_core_routers(app)
|
||||
|
||||
for ext in get_valid_extensions(False):
|
||||
try:
|
||||
register_ext_routes(app, ext)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not load extension `{ext.code}`: {str(e)}")
|
||||
|
||||
|
||||
def register_custom_extensions_path():
|
||||
upgrades_dir = settings.lnbits_extensions_upgrade_path
|
||||
shutil.rmtree(upgrades_dir, True)
|
||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||
sys.path.append(str(upgrades_dir))
|
||||
|
||||
if settings.has_default_extension_path:
|
||||
return
|
||||
default_ext_path = os.path.join("lnbits", "extensions")
|
||||
@@ -311,8 +389,9 @@ def register_custom_extensions_path():
|
||||
+ f" '{settings.lnbits_extensions_path}/extensions'"
|
||||
)
|
||||
|
||||
sys.path.append(str(Path(settings.lnbits_extensions_path, "extensions")))
|
||||
sys.path.append(str(Path(settings.lnbits_extensions_path, "upgrades")))
|
||||
extensions_dir = Path(settings.lnbits_extensions_path, "extensions")
|
||||
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
|
||||
sys.path.append(str(extensions_dir))
|
||||
|
||||
|
||||
def register_new_ext_routes(app: FastAPI) -> Callable:
|
||||
@@ -338,16 +417,21 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
||||
return register_new_ratelimiter_fn
|
||||
|
||||
|
||||
def register_ext_tasks(ext: Extension) -> None:
|
||||
"""Register extension async tasks."""
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
||||
ext_start_func()
|
||||
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
|
||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_start"):
|
||||
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
||||
ext_start_func()
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_static_files"):
|
||||
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
||||
for s in ext_statics:
|
||||
@@ -356,274 +440,54 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
)
|
||||
app.mount(s["path"], StaticFiles(directory=static_dir), s["name"])
|
||||
|
||||
if hasattr(ext_module, f"{ext.code}_redirect_paths"):
|
||||
ext_redirects = getattr(ext_module, f"{ext.code}_redirect_paths")
|
||||
settings.lnbits_extensions_redirects = [
|
||||
r for r in settings.lnbits_extensions_redirects if r["ext_id"] != ext.code
|
||||
]
|
||||
for r in ext_redirects:
|
||||
r["ext_id"] = ext.code
|
||||
settings.lnbits_extensions_redirects.append(r)
|
||||
ext_redirects = (
|
||||
getattr(ext_module, f"{ext.code}_redirect_paths")
|
||||
if hasattr(ext_module, f"{ext.code}_redirect_paths")
|
||||
else []
|
||||
)
|
||||
|
||||
logger.trace(f"adding route for extension {ext_module}")
|
||||
settings.activate_extension_paths(ext.code, ext.upgrade_hash, ext_redirects)
|
||||
|
||||
logger.trace(f"Adding route for extension {ext_module}.")
|
||||
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash != "" else ""
|
||||
app.include_router(router=ext_route, prefix=prefix)
|
||||
|
||||
|
||||
def register_startup(app: FastAPI):
|
||||
@app.on_event("startup")
|
||||
async def lnbits_startup():
|
||||
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||
await check_installed_extensions(app)
|
||||
for ext in await get_valid_extensions(False):
|
||||
try:
|
||||
# wait till migration is done
|
||||
await migrate_databases()
|
||||
|
||||
# setup admin settings
|
||||
await check_admin_settings()
|
||||
await check_webpush_settings()
|
||||
|
||||
log_server_info()
|
||||
|
||||
# initialize WALLET
|
||||
try:
|
||||
set_wallet_class()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error initializing {settings.lnbits_backend_wallet_class}: {e}"
|
||||
)
|
||||
set_void_wallet_class()
|
||||
|
||||
# initialize funding source
|
||||
await check_funding_source()
|
||||
|
||||
# check extensions after restart
|
||||
await check_installed_extensions(app)
|
||||
|
||||
# register core and extension routes
|
||||
register_routes(app)
|
||||
|
||||
if settings.lnbits_admin_ui:
|
||||
initialize_server_logger()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
raise ImportError("Failed to run 'startup' event.")
|
||||
register_ext_routes(app, ext)
|
||||
register_ext_tasks(ext)
|
||||
except Exception as exc:
|
||||
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
||||
|
||||
|
||||
def register_shutdown(app: FastAPI):
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown():
|
||||
cancel_all_tasks()
|
||||
# wait a bit to allow them to finish, so that cleanup can run without problems
|
||||
await asyncio.sleep(0.1)
|
||||
WALLET = get_wallet_class()
|
||||
await WALLET.cleanup()
|
||||
def register_async_tasks() -> None:
|
||||
|
||||
create_permanent_task(wait_for_audit_data)
|
||||
create_permanent_task(wait_notification_messages)
|
||||
|
||||
def initialize_server_logger():
|
||||
super_user_hash = sha256(settings.super_user.encode("utf-8")).hexdigest()
|
||||
|
||||
serverlog_queue = asyncio.Queue()
|
||||
|
||||
async def update_websocket_serverlog():
|
||||
while True:
|
||||
msg = await serverlog_queue.get()
|
||||
await websocketUpdater(super_user_hash, msg)
|
||||
|
||||
create_permanent_task(update_websocket_serverlog)
|
||||
|
||||
logger.add(
|
||||
lambda msg: serverlog_queue.put_nowait(msg),
|
||||
format=Formatter().format,
|
||||
)
|
||||
|
||||
|
||||
def log_server_info():
|
||||
logger.info("Starting LNbits")
|
||||
logger.info(f"Version: {settings.version}")
|
||||
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
||||
logger.info(f"Host: {settings.host}")
|
||||
logger.info(f"Port: {settings.port}")
|
||||
logger.info(f"Debug: {settings.debug}")
|
||||
logger.info(f"Site title: {settings.lnbits_site_title}")
|
||||
logger.info(f"Funding source: {settings.lnbits_backend_wallet_class}")
|
||||
logger.info(f"Data folder: {settings.lnbits_data_folder}")
|
||||
logger.info(f"Database: {get_db_vendor_name()}")
|
||||
logger.info(f"Service fee: {settings.lnbits_service_fee}")
|
||||
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
|
||||
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
|
||||
|
||||
|
||||
def get_db_vendor_name():
|
||||
db_url = settings.lnbits_database_url
|
||||
return (
|
||||
"PostgreSQL"
|
||||
if db_url and db_url.startswith("postgres://")
|
||||
else (
|
||||
"CockroachDB"
|
||||
if db_url and db_url.startswith("cockroachdb://")
|
||||
else "SQLite"
|
||||
create_permanent_task(
|
||||
run_interval(
|
||||
settings.lnbits_funding_source_pending_interval_seconds,
|
||||
check_pending_payments,
|
||||
)
|
||||
)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
def register_async_tasks(app):
|
||||
@app.on_event("startup")
|
||||
async def listeners():
|
||||
create_permanent_task(check_pending_payments)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
create_permanent_task(cache.invalidate_forever)
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
create_permanent_task(purge_audit_data)
|
||||
create_permanent_task(collect_exchange_rates_data)
|
||||
|
||||
# core invoice listener
|
||||
invoice_queue = asyncio.Queue(5)
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
# TODO: implement watchdog properly
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
@app.exception_handler(Exception)
|
||||
async def exception_handler(request: Request, exc: Exception):
|
||||
etype, _, tb = sys.exc_info()
|
||||
traceback.print_exception(etype, exc, tb)
|
||||
logger.error(f"Exception: {str(exc)}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": f"Error: {str(exc)}"}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
):
|
||||
logger.error(f"RequestValidationError: {str(exc)}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{"request": request, "err": f"Error: {str(exc)}"},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
if exc.headers and "token-expired" in exc.headers:
|
||||
response = RedirectResponse("/")
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{
|
||||
"request": request,
|
||||
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
)
|
||||
|
||||
|
||||
def configure_logger() -> None:
|
||||
logger.remove()
|
||||
log_level: str = "DEBUG" if settings.debug else "INFO"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stdout, level=log_level, format=formatter.format)
|
||||
|
||||
if settings.enable_log_to_file:
|
||||
logger.add(
|
||||
Path(settings.lnbits_data_folder, "logs", "lnbits.log"),
|
||||
rotation=settings.log_rotation,
|
||||
retention=settings.log_retention,
|
||||
level="INFO",
|
||||
format=formatter.format,
|
||||
)
|
||||
logger.add(
|
||||
Path(settings.lnbits_data_folder, "logs", "debug.log"),
|
||||
rotation=settings.log_rotation,
|
||||
retention=settings.log_retention,
|
||||
level="DEBUG",
|
||||
format=formatter.format,
|
||||
)
|
||||
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.error").propagate = False
|
||||
|
||||
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base").propagate = False
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
self.padding = 0
|
||||
self.minimal_fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | "
|
||||
"<level>{message}</level>\n"
|
||||
)
|
||||
if settings.debug:
|
||||
self.fmt = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | "
|
||||
"<level>{level: <4}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
||||
"<level>{message}</level>\n"
|
||||
)
|
||||
else:
|
||||
self.fmt = self.minimal_fmt
|
||||
|
||||
def format(self, record):
|
||||
function = "{function}".format(**record)
|
||||
if function == "emit": # uvicorn logs
|
||||
return self.minimal_fmt
|
||||
return self.fmt
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
logger.log(level, record.getMessage())
|
||||
# server logs for websocket
|
||||
if settings.lnbits_admin_ui:
|
||||
server_log_task = initialize_server_websocket_logger()
|
||||
create_permanent_task(server_log_task)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
import click
|
||||
import httpx
|
||||
@@ -10,30 +13,36 @@ from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
from packaging import version
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.services import check_admin_settings
|
||||
from lnbits.core.views.api import api_install_extension, api_uninstall_extension
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .core import db as core_db
|
||||
from .core import migrations as core_migrations
|
||||
from .core.crud import (
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import (
|
||||
delete_accounts_no_wallets,
|
||||
delete_unused_wallets,
|
||||
get_dbversions,
|
||||
get_inactive_extensions,
|
||||
delete_wallet_by_id,
|
||||
delete_wallet_payment,
|
||||
get_db_versions,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_payment,
|
||||
get_payments,
|
||||
remove_deleted_wallets,
|
||||
update_payment,
|
||||
)
|
||||
from .core.helpers import migrate_extension_database, run_migration
|
||||
from .db import COCKROACH, POSTGRES, SQLITE
|
||||
from .extension_manager import (
|
||||
from lnbits.core.helpers import is_valid_url, migrate_databases
|
||||
from lnbits.core.models import Account, Payment, PaymentState
|
||||
from lnbits.core.models.extensions import (
|
||||
CreateExtension,
|
||||
ExtensionRelease,
|
||||
InstallableExtension,
|
||||
get_valid_extensions,
|
||||
)
|
||||
from lnbits.core.services import check_admin_settings, create_user_account_no_ckeck
|
||||
from lnbits.core.views.extension_api import (
|
||||
api_install_extension,
|
||||
api_uninstall_extension,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.wallets.base import Wallet
|
||||
from lnbits.wallets.macaroon import load_macaroon
|
||||
|
||||
|
||||
def coro(f):
|
||||
@@ -58,6 +67,13 @@ def db():
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def users():
|
||||
"""
|
||||
Users related commands
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def extensions():
|
||||
"""
|
||||
@@ -65,7 +81,21 @@ def extensions():
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> Optional[str]:
|
||||
@lnbits_cli.group()
|
||||
def encrypt():
|
||||
"""
|
||||
Encryption commands
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def decrypt():
|
||||
"""
|
||||
Decryption commands
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> str | None:
|
||||
"""Get the superuser"""
|
||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||
if not superuser_file.exists() or not superuser_file.is_file():
|
||||
@@ -73,7 +103,7 @@ def get_super_user() -> Optional[str]:
|
||||
"Superuser id not found. Please check that the file "
|
||||
+ f"'{superuser_file.absolute()}' exists and has read permissions."
|
||||
)
|
||||
with open(superuser_file, "r") as file:
|
||||
with open(superuser_file) as file:
|
||||
return file.readline()
|
||||
|
||||
|
||||
@@ -103,7 +133,7 @@ async def delete_settings():
|
||||
"""Deletes the settings"""
|
||||
|
||||
async with core_db.connect() as conn:
|
||||
await conn.execute("DELETE from settings")
|
||||
await conn.execute("DELETE from system_settings")
|
||||
|
||||
|
||||
@db.command("migrate")
|
||||
@@ -113,58 +143,18 @@ def database_migrate():
|
||||
loop.run_until_complete(migrate_databases())
|
||||
|
||||
|
||||
async def db_migrate():
|
||||
asyncio.create_task(migrate_databases())
|
||||
|
||||
|
||||
async def migrate_databases():
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
async with core_db.connect() as conn:
|
||||
exists = False
|
||||
if conn.type == SQLITE:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
|
||||
)
|
||||
elif conn.type in {POSTGRES, COCKROACH}:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
|
||||
" AND table_name = 'dbversions'"
|
||||
)
|
||||
|
||||
if not exists:
|
||||
await core_migrations.m000_create_migrations_table(conn)
|
||||
|
||||
current_versions = await get_dbversions(conn)
|
||||
core_version = current_versions.get("core", 0)
|
||||
await run_migration(conn, core_migrations, "core", core_version)
|
||||
|
||||
# here is the first place we can be sure that the
|
||||
# `installed_extensions` table has been created
|
||||
await load_disabled_extension_list()
|
||||
|
||||
for ext in get_valid_extensions(False):
|
||||
current_version = current_versions.get(ext.code, 0)
|
||||
try:
|
||||
await migrate_extension_database(ext, current_version)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error migrating extension {ext.code}: {e}")
|
||||
|
||||
logger.info("✔️ All migrations done.")
|
||||
|
||||
|
||||
@db.command("versions")
|
||||
@coro
|
||||
async def db_versions():
|
||||
"""Show current database versions"""
|
||||
async with core_db.connect() as conn:
|
||||
click.echo(await get_dbversions(conn))
|
||||
click.echo(await get_db_versions(conn))
|
||||
|
||||
|
||||
@db.command("cleanup-wallets")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_wallets(days: Optional[int] = None):
|
||||
async def database_cleanup_wallets(days: int | None = None):
|
||||
"""Delete all wallets that never had any transaction"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
@@ -180,10 +170,139 @@ async def database_cleanup_deleted_wallets():
|
||||
await remove_deleted_wallets(conn)
|
||||
|
||||
|
||||
@db.command("cleanup-accounts")
|
||||
@db.command("delete-wallet")
|
||||
@click.option("-w", "--wallet", required=True, help="ID of wallet to be deleted.")
|
||||
@coro
|
||||
async def database_delete_wallet(wallet: str):
|
||||
"""Mark wallet as deleted"""
|
||||
async with core_db.connect() as conn:
|
||||
count = await delete_wallet_by_id(wallet_id=wallet, conn=conn)
|
||||
click.echo(f"Marked as deleted '{count}' rows.")
|
||||
|
||||
|
||||
@db.command("delete-wallet-payment")
|
||||
@click.option("-w", "--wallet", required=True, help="ID of wallet to be deleted.")
|
||||
@click.option("-c", "--checking-id", required=True, help="Payment checking Id.")
|
||||
@coro
|
||||
async def database_delete_wallet_payment(wallet: str, checking_id: str):
|
||||
"""Delete wallet payment"""
|
||||
async with core_db.connect() as conn:
|
||||
await delete_wallet_payment(
|
||||
wallet_id=wallet, checking_id=checking_id, conn=conn
|
||||
)
|
||||
|
||||
|
||||
@db.command("mark-payment-pending")
|
||||
@click.option("-c", "--checking-id", required=True, help="Payment checking Id.")
|
||||
@coro
|
||||
async def database_revert_payment(checking_id: str):
|
||||
"""Mark payment as pending"""
|
||||
async with core_db.connect() as conn:
|
||||
payment = await get_payment(checking_id=checking_id, conn=conn)
|
||||
payment.status = PaymentState.PENDING
|
||||
await update_payment(payment, conn=conn)
|
||||
click.echo(f"Payment '{checking_id}' marked as pending.")
|
||||
|
||||
|
||||
@db.command("check-payments")
|
||||
@click.option("-d", "--days", help="Maximum age of payments in days.")
|
||||
@click.option("-l", "--limit", help="Maximum number of payments to be checked.")
|
||||
@click.option("-w", "--wallet", help="Only check for this wallet.")
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Detailed log.")
|
||||
@coro
|
||||
async def check_invalid_payments(
|
||||
days: int | None = None,
|
||||
limit: int | None = None,
|
||||
wallet: str | None = None,
|
||||
verbose: bool | None = False,
|
||||
):
|
||||
"""Check payments that are settled in the DB but pending on the Funding Source"""
|
||||
await check_admin_settings()
|
||||
settled_db_payments = []
|
||||
|
||||
if verbose:
|
||||
click.echo(f"Get Payments: days={days}, limit={limit}, wallet={wallet}")
|
||||
async with core_db.connect() as conn:
|
||||
delta = int(days) if days else 3 # default to 3 days
|
||||
limit = int(limit) if limit else 1000
|
||||
since = int(time.time()) - delta * 24 * 60 * 60
|
||||
|
||||
settled_db_payments = await get_payments(
|
||||
complete=True,
|
||||
incoming=True,
|
||||
exclude_uncheckable=True,
|
||||
since=since,
|
||||
limit=limit,
|
||||
wallet_id=wallet,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
click.echo("Settled Payments: " + str(len(settled_db_payments)))
|
||||
|
||||
wallets_module = importlib.import_module("lnbits.wallets")
|
||||
wallet_class = getattr(wallets_module, settings.lnbits_backend_wallet_class)
|
||||
|
||||
funding_source: Wallet = wallet_class()
|
||||
|
||||
click.echo("Funding source: " + str(funding_source))
|
||||
|
||||
# payments that are settled in the DB, but not at the Funding source level
|
||||
invalid_payments: list[Payment] = []
|
||||
invalid_wallets = {}
|
||||
for db_payment in settled_db_payments:
|
||||
if verbose:
|
||||
click.echo(
|
||||
f"Checking Payment: '{db_payment.checking_id}' for wallet"
|
||||
+ f" '{db_payment.wallet_id}'."
|
||||
)
|
||||
payment_status = await funding_source.get_invoice_status(db_payment.checking_id)
|
||||
|
||||
if payment_status.pending:
|
||||
invalid_payments.append(db_payment)
|
||||
if db_payment.wallet_id not in invalid_wallets:
|
||||
invalid_wallets[f"{db_payment.wallet_id}"] = [0, 0]
|
||||
invalid_wallets[f"{db_payment.wallet_id}"][0] += 1
|
||||
invalid_wallets[f"{db_payment.wallet_id}"][1] += db_payment.amount
|
||||
|
||||
click.echo(
|
||||
"Invalid Payment: '"
|
||||
+ " ".join(
|
||||
[
|
||||
db_payment.checking_id,
|
||||
db_payment.wallet_id,
|
||||
str(db_payment.amount / 1000).ljust(10),
|
||||
db_payment.memo or "",
|
||||
]
|
||||
)
|
||||
+ "'"
|
||||
)
|
||||
|
||||
click.echo("Invalid Payments: " + str(len(invalid_payments)))
|
||||
click.echo("\nInvalid Wallets: " + str(len(invalid_wallets)))
|
||||
for w in invalid_wallets:
|
||||
data = invalid_wallets[f"{w}"]
|
||||
click.echo(" ".join([w, str(data[0]), str(data[1] / 1000).ljust(10)]))
|
||||
|
||||
|
||||
@users.command("new")
|
||||
@click.option("-u", "--username", required=True, help="Username.")
|
||||
@click.option("-p", "--password", required=True, help="Password.")
|
||||
@coro
|
||||
async def create_user(username: str, password: str):
|
||||
"""Create a new user bypassing the system 'new_accounts_allowed' rules"""
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=username,
|
||||
)
|
||||
account.hash_password(password)
|
||||
user = await create_user_account_no_ckeck(account)
|
||||
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
|
||||
|
||||
|
||||
@users.command("cleanup-accounts")
|
||||
@click.argument("days", type=int, required=False)
|
||||
@coro
|
||||
async def database_cleanup_accounts(days: Optional[int] = None):
|
||||
async def database_cleanup_accounts(days: int | None = None):
|
||||
"""Delete all accounts that have no wallets"""
|
||||
async with core_db.connect() as conn:
|
||||
delta = days or settings.cleanup_wallets_days
|
||||
@@ -191,12 +310,6 @@ async def database_cleanup_accounts(days: Optional[int] = None):
|
||||
await delete_accounts_no_wallets(delta, conn)
|
||||
|
||||
|
||||
async def load_disabled_extension_list() -> None:
|
||||
"""Update list of extensions that have been explicitly disabled"""
|
||||
inactive_extensions = await get_inactive_extensions()
|
||||
settings.lnbits_deactivated_extensions += inactive_extensions
|
||||
|
||||
|
||||
@extensions.command("list")
|
||||
@coro
|
||||
async def extensions_list():
|
||||
@@ -206,13 +319,14 @@ async def extensions_list():
|
||||
from lnbits.app import build_all_installed_extensions_list
|
||||
|
||||
for ext in await build_all_installed_extensions_list():
|
||||
assert ext.installed_release, f"Extension {ext.id} has no installed_release"
|
||||
click.echo(f" - {ext.id} ({ext.installed_release.version})")
|
||||
if not ext.meta or not ext.meta.installed_release:
|
||||
raise ValueError(f"Extension {ext.id} has no installed_release")
|
||||
click.echo(f" - {ext.id} ({ext.meta.installed_release.version})")
|
||||
|
||||
|
||||
@extensions.command("update")
|
||||
@click.argument("extension", required=False)
|
||||
@click.option("-a", "--all", is_flag=True, help="Update all extensions.")
|
||||
@click.option("-a", "--all-extensions", is_flag=True, help="Update all extensions.")
|
||||
@click.option(
|
||||
"-i", "--repo-index", help="Select the index of the repository to be used."
|
||||
)
|
||||
@@ -237,38 +351,43 @@ async def extensions_list():
|
||||
help="Admin user ID (must have permissions to install extensions).",
|
||||
)
|
||||
@coro
|
||||
async def extensions_update(
|
||||
extension: Optional[str] = None,
|
||||
all: Optional[bool] = False,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
async def extensions_update( # noqa: C901
|
||||
extension: str | None = None,
|
||||
all_extensions: bool | None = False,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
):
|
||||
"""
|
||||
Update extension to the latest version.
|
||||
If an extension is not present it will be instaled.
|
||||
"""
|
||||
if not extension and not all:
|
||||
if not extension and not all_extensions:
|
||||
click.echo("Extension ID is required.")
|
||||
click.echo("Or specify the '--all' flag to update all extensions")
|
||||
click.echo("Or specify the '--all-extensions' flag to update all extensions")
|
||||
return
|
||||
if extension and all:
|
||||
if extension and all_extensions:
|
||||
click.echo("Only one of extension ID or the '--all' flag must be specified")
|
||||
return
|
||||
if url and not _is_url(url):
|
||||
if url and not is_valid_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
if not await _can_run_operation(url):
|
||||
return
|
||||
|
||||
upgrades_dir = settings.lnbits_extensions_upgrade_path
|
||||
Path(upgrades_dir).mkdir(parents=True, exist_ok=True)
|
||||
sys.path.append(str(upgrades_dir))
|
||||
|
||||
if extension:
|
||||
await update_extension(extension, repo_index, source_repo, url, admin_user)
|
||||
return
|
||||
|
||||
click.echo("Updating all extensions...")
|
||||
click.echo("Updating all extensions:")
|
||||
installed_extensions = await get_installed_extensions()
|
||||
click.echo(f" {[e.id for e in installed_extensions]}")
|
||||
updated_extensions = []
|
||||
for e in installed_extensions:
|
||||
try:
|
||||
@@ -323,14 +442,14 @@ async def extensions_update(
|
||||
@coro
|
||||
async def extensions_install(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
):
|
||||
"""Install a extension"""
|
||||
click.echo(f"Installing {extension}... {repo_index}")
|
||||
if url and not _is_url(url):
|
||||
if url and not is_valid_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
@@ -353,12 +472,12 @@ async def extensions_install(
|
||||
)
|
||||
@coro
|
||||
async def extensions_uninstall(
|
||||
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None
|
||||
extension: str, url: str | None = None, admin_user: str | None = None
|
||||
):
|
||||
"""Uninstall a extension"""
|
||||
click.echo(f"Uninstalling '{extension}'...")
|
||||
|
||||
if url and not _is_url(url):
|
||||
if url and not is_valid_url(url):
|
||||
click.echo(f"Invalid '--url' option value: {url}")
|
||||
return
|
||||
|
||||
@@ -371,10 +490,66 @@ async def extensions_uninstall(
|
||||
click.echo(f"Failed to uninstall '{extension}' Error: '{ex.detail}'.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to uninstall '{extension}': {str(ex)}.")
|
||||
click.echo(f"Failed to uninstall '{extension}': {ex!s}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
@encrypt.command("macaroon")
|
||||
def encrypt_macaroon():
|
||||
"""Encrypts a macaroon (LND wallets)"""
|
||||
_macaroon = getpass("Enter macaroon: ")
|
||||
try:
|
||||
macaroon = load_macaroon(_macaroon)
|
||||
except Exception as ex:
|
||||
click.echo(f"Error loading macaroon: {ex}")
|
||||
return
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
encrypted_macaroon = aes.encrypt(bytes.fromhex(macaroon))
|
||||
except Exception as ex:
|
||||
click.echo(f"Error encrypting macaroon: {ex}")
|
||||
return
|
||||
click.echo("Encrypted macaroon: ")
|
||||
click.echo(encrypted_macaroon)
|
||||
|
||||
|
||||
@encrypt.command("aes")
|
||||
@click.option("-p", "--payload", required=True, help="Payload to encrypt.")
|
||||
@click.option(
|
||||
"-u", "--urlsafe", is_flag=True, required=False, help="Urlsafe b64encode."
|
||||
)
|
||||
def encrypt_aes(payload: str, urlsafe: bool = False):
|
||||
"""AES encrypts a payload"""
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
encrypted = aes.encrypt(payload.encode(), urlsafe=urlsafe)
|
||||
except Exception as ex:
|
||||
click.echo(f"Error encrypting payload: {ex}")
|
||||
return
|
||||
click.echo("Encrypted payload: ")
|
||||
click.echo(encrypted)
|
||||
|
||||
|
||||
@decrypt.command("aes")
|
||||
@click.option("-p", "--payload", required=True, help="Payload to decrypt.")
|
||||
@click.option(
|
||||
"-u", "--urlsafe", is_flag=True, required=False, help="Urlsafe b64decode."
|
||||
)
|
||||
def decrypt_aes(payload: str, urlsafe: bool = False):
|
||||
"""AES decrypts a payload"""
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
decrypted = aes.decrypt(payload, urlsafe=urlsafe)
|
||||
except Exception as ex:
|
||||
click.echo(f"Error decrypting payload: {ex}")
|
||||
return
|
||||
click.echo("Decrypted payload: ")
|
||||
click.echo(decrypted)
|
||||
|
||||
|
||||
def main():
|
||||
"""main function"""
|
||||
lnbits_cli()
|
||||
@@ -386,11 +561,11 @@ if __name__ == "__main__":
|
||||
|
||||
async def install_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
try:
|
||||
release = await _select_release(extension, repo_index, source_repo)
|
||||
if not release:
|
||||
@@ -409,17 +584,17 @@ async def install_extension(
|
||||
click.echo(f"Failed to install '{extension}' Error: '{ex.detail}'.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to install '{extension}': {str(ex)}.")
|
||||
click.echo(f"Failed to install '{extension}': {ex!s}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
async def update_extension(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
admin_user: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
url: str | None = None,
|
||||
admin_user: str | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
try:
|
||||
click.echo(f"Updating '{extension}' extension.")
|
||||
installed_ext = await get_installed_extension(extension)
|
||||
@@ -431,16 +606,17 @@ async def update_extension(
|
||||
|
||||
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
|
||||
|
||||
assert (
|
||||
installed_ext.installed_release
|
||||
), "Cannot find previously installed release. Please uninstall first."
|
||||
if not installed_ext.meta or not installed_ext.meta.installed_release:
|
||||
raise ValueError(
|
||||
"Cannot find previously installed release. Please uninstall first."
|
||||
)
|
||||
|
||||
release = await _select_release(extension, repo_index, source_repo)
|
||||
if not release:
|
||||
return False, "No release selected."
|
||||
if (
|
||||
release.version == installed_ext.installed_version
|
||||
and release.source_repo == installed_ext.installed_release.source_repo
|
||||
and release.source_repo == installed_ext.meta.installed_release.source_repo
|
||||
):
|
||||
click.echo(f"Extension '{extension}' already up to date.")
|
||||
return False, "Already up to date"
|
||||
@@ -461,15 +637,15 @@ async def update_extension(
|
||||
click.echo(f"Failed to update '{extension}' Error: '{ex.detail}.")
|
||||
return False, ex.detail
|
||||
except Exception as ex:
|
||||
click.echo(f"Failed to update '{extension}': {str(ex)}.")
|
||||
click.echo(f"Failed to update '{extension}': {ex!s}.")
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
async def _select_release(
|
||||
extension: str,
|
||||
repo_index: Optional[str] = None,
|
||||
source_repo: Optional[str] = None,
|
||||
) -> Optional[ExtensionRelease]:
|
||||
repo_index: str | None = None,
|
||||
source_repo: str | None = None,
|
||||
) -> ExtensionRelease | None:
|
||||
all_releases = await InstallableExtension.get_extension_releases(extension)
|
||||
if len(all_releases) == 0:
|
||||
click.echo(f"No repository found for extension '{extension}'.")
|
||||
@@ -484,7 +660,7 @@ async def _select_release(
|
||||
return latest_repo_releases[source_repo]
|
||||
|
||||
if len(latest_repo_releases) == 1:
|
||||
return latest_repo_releases[list(latest_repo_releases.keys())[0]]
|
||||
return latest_repo_releases[next(iter(latest_repo_releases.keys()))]
|
||||
|
||||
repos = list(latest_repo_releases.keys())
|
||||
repos.sort()
|
||||
@@ -529,7 +705,7 @@ def _get_latest_release_per_repo(all_releases):
|
||||
|
||||
|
||||
async def _call_install_extension(
|
||||
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
|
||||
data: CreateExtension, url: str | None, user_id: str | None = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
@@ -539,11 +715,11 @@ async def _call_install_extension(
|
||||
)
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
await api_install_extension(data, User(id="mock_id"))
|
||||
await api_install_extension(data)
|
||||
|
||||
|
||||
async def _call_uninstall_extension(
|
||||
extension: str, url: Optional[str], user_id: Optional[str] = None
|
||||
extension: str, url: str | None, user_id: str | None = None
|
||||
):
|
||||
if url:
|
||||
user_id = user_id or get_super_user()
|
||||
@@ -553,7 +729,7 @@ async def _call_uninstall_extension(
|
||||
)
|
||||
resp.raise_for_status()
|
||||
else:
|
||||
await api_uninstall_extension(extension, User(id="mock_id"))
|
||||
await api_uninstall_extension(extension)
|
||||
|
||||
|
||||
async def _can_run_operation(url) -> bool:
|
||||
@@ -572,14 +748,14 @@ async def _can_run_operation(url) -> bool:
|
||||
elif url:
|
||||
click.echo(
|
||||
"The option '--url' has been provided,"
|
||||
+ f" but no server found runnint at '{url}'"
|
||||
f" but no server found running at '{url}'"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _is_lnbits_started(url: Optional[str]):
|
||||
async def _is_lnbits_started(url: str | None):
|
||||
try:
|
||||
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -587,11 +763,3 @@ async def _is_lnbits_started(url: Optional[str]):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _is_url(url):
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -1,30 +1,53 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, FastAPI
|
||||
|
||||
from .db import core_app_extra, db
|
||||
from .views.admin_api import admin_router
|
||||
from .views.api import api_router
|
||||
from .views.asset_api import asset_router
|
||||
from .views.audit_api import audit_router
|
||||
from .views.auth_api import auth_router
|
||||
from .views.callback_api import callback_router
|
||||
from .views.extension_api import extension_router
|
||||
from .views.extensions_builder_api import extension_builder_router
|
||||
from .views.fiat_api import fiat_router
|
||||
|
||||
# this compat is needed for usermanager extension
|
||||
from .views.generic import generic_router, update_user_extension
|
||||
from .views.generic import generic_router
|
||||
from .views.lnurl_api import lnurl_router
|
||||
from .views.node_api import node_router, public_node_router, super_node_router
|
||||
from .views.public_api import public_router
|
||||
from .views.payment_api import payment_router
|
||||
from .views.tinyurl_api import tinyurl_router
|
||||
from .views.user_api import users_router
|
||||
from .views.wallet_api import wallet_router
|
||||
from .views.webpush_api import webpush_router
|
||||
from .views.websocket_api import websocket_router
|
||||
|
||||
# backwards compatibility for extensions
|
||||
core_app = APIRouter(tags=["Core"])
|
||||
|
||||
|
||||
def init_core_routers(app):
|
||||
def init_core_routers(app: FastAPI):
|
||||
app.include_router(core_app)
|
||||
app.include_router(generic_router)
|
||||
app.include_router(public_router)
|
||||
app.include_router(api_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(node_router)
|
||||
app.include_router(extension_router)
|
||||
app.include_router(extension_builder_router)
|
||||
app.include_router(super_node_router)
|
||||
app.include_router(public_node_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(payment_router)
|
||||
app.include_router(wallet_router)
|
||||
app.include_router(api_router)
|
||||
app.include_router(websocket_router)
|
||||
app.include_router(callback_router)
|
||||
app.include_router(tinyurl_router)
|
||||
app.include_router(webpush_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(audit_router)
|
||||
app.include_router(asset_router)
|
||||
app.include_router(fiat_router)
|
||||
app.include_router(lnurl_router)
|
||||
|
||||
|
||||
__all__ = ["core_app", "core_app_extra", "db"]
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
from .audit import create_audit_entry
|
||||
from .db_versions import (
|
||||
delete_dbversion,
|
||||
get_db_version,
|
||||
get_db_versions,
|
||||
update_migration_version,
|
||||
)
|
||||
from .extensions import (
|
||||
create_installed_extension,
|
||||
create_user_extension,
|
||||
delete_installed_extension,
|
||||
drop_extension_db,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_installed_extensions_count,
|
||||
get_user_active_extensions_ids,
|
||||
get_user_extension,
|
||||
get_user_extensions,
|
||||
update_installed_extension,
|
||||
update_installed_extension_state,
|
||||
update_user_extension,
|
||||
)
|
||||
from .payments import (
|
||||
DateTrunc,
|
||||
check_internal,
|
||||
create_payment,
|
||||
delete_expired_invoices,
|
||||
delete_wallet_payment,
|
||||
get_latest_payments_by_extension,
|
||||
get_payment,
|
||||
get_payments,
|
||||
get_payments_history,
|
||||
get_payments_paginated,
|
||||
get_standalone_payment,
|
||||
get_wallet_payment,
|
||||
is_internal_status_success,
|
||||
mark_webhook_sent,
|
||||
update_payment,
|
||||
update_payment_checking_id,
|
||||
update_payment_extra,
|
||||
)
|
||||
from .settings import (
|
||||
create_admin_settings,
|
||||
delete_admin_settings,
|
||||
get_admin_settings,
|
||||
get_super_settings,
|
||||
reset_core_settings,
|
||||
update_admin_settings,
|
||||
update_super_user,
|
||||
)
|
||||
from .tinyurl import create_tinyurl, delete_tinyurl, get_tinyurl, get_tinyurl_by_url
|
||||
from .users import (
|
||||
create_account,
|
||||
delete_account,
|
||||
delete_accounts_no_wallets,
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_pubkey,
|
||||
get_account_by_username,
|
||||
get_account_by_username_or_email,
|
||||
get_accounts,
|
||||
get_accounts_count,
|
||||
get_user,
|
||||
get_user_access_control_lists,
|
||||
get_user_from_account,
|
||||
update_account,
|
||||
)
|
||||
from .wallets import (
|
||||
create_wallet,
|
||||
delete_unused_wallets,
|
||||
delete_wallet,
|
||||
delete_wallet_by_id,
|
||||
force_delete_wallet,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
get_wallets,
|
||||
remove_deleted_wallets,
|
||||
update_wallet,
|
||||
)
|
||||
from .webpush import (
|
||||
create_webpush_subscription,
|
||||
delete_webpush_subscription,
|
||||
delete_webpush_subscriptions,
|
||||
get_webpush_subscription,
|
||||
get_webpush_subscriptions_for_user,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DateTrunc",
|
||||
"check_internal",
|
||||
"create_account",
|
||||
"create_admin_settings",
|
||||
"create_audit_entry",
|
||||
"create_installed_extension",
|
||||
"create_payment",
|
||||
"create_tinyurl",
|
||||
"create_user_extension",
|
||||
"create_wallet",
|
||||
"create_webpush_subscription",
|
||||
"delete_account",
|
||||
"delete_accounts_no_wallets",
|
||||
"delete_admin_settings",
|
||||
"delete_dbversion",
|
||||
"delete_expired_invoices",
|
||||
"delete_installed_extension",
|
||||
"delete_tinyurl",
|
||||
"delete_unused_wallets",
|
||||
"delete_wallet",
|
||||
"delete_wallet_by_id",
|
||||
"delete_wallet_payment",
|
||||
"delete_webpush_subscription",
|
||||
"delete_webpush_subscriptions",
|
||||
"drop_extension_db",
|
||||
"force_delete_wallet",
|
||||
"get_account",
|
||||
"get_account_by_email",
|
||||
"get_account_by_pubkey",
|
||||
"get_account_by_username",
|
||||
"get_account_by_username_or_email",
|
||||
"get_accounts",
|
||||
"get_accounts_count",
|
||||
"get_admin_settings",
|
||||
"get_db_version",
|
||||
"get_db_versions",
|
||||
"get_installed_extension",
|
||||
"get_installed_extensions",
|
||||
"get_installed_extensions_count",
|
||||
"get_latest_payments_by_extension",
|
||||
"get_payment",
|
||||
"get_payments",
|
||||
"get_payments_history",
|
||||
"get_payments_paginated",
|
||||
"get_standalone_payment",
|
||||
"get_super_settings",
|
||||
"get_tinyurl",
|
||||
"get_tinyurl_by_url",
|
||||
"get_total_balance",
|
||||
"get_user",
|
||||
"get_user_access_control_lists",
|
||||
"get_user_active_extensions_ids",
|
||||
"get_user_extension",
|
||||
"get_user_extensions",
|
||||
"get_user_from_account",
|
||||
"get_wallet",
|
||||
"get_wallet_for_key",
|
||||
"get_wallet_payment",
|
||||
"get_wallets",
|
||||
"get_webpush_subscription",
|
||||
"get_webpush_subscriptions_for_user",
|
||||
"is_internal_status_success",
|
||||
"mark_webhook_sent",
|
||||
"remove_deleted_wallets",
|
||||
"reset_core_settings",
|
||||
"update_account",
|
||||
"update_admin_settings",
|
||||
"update_installed_extension",
|
||||
"update_installed_extension_state",
|
||||
"update_migration_version",
|
||||
"update_payment",
|
||||
"update_payment_checking_id",
|
||||
"update_payment_extra",
|
||||
"update_super_user",
|
||||
"update_user_extension",
|
||||
"update_wallet",
|
||||
]
|
||||
@@ -0,0 +1,108 @@
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.assets import Asset, AssetFilters, AssetInfo
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
|
||||
|
||||
async def create_asset(
|
||||
entry: Asset,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("assets", entry)
|
||||
|
||||
|
||||
async def get_user_asset_info(
|
||||
user_id: str,
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def get_asset_info(
|
||||
asset_id: str, conn: Connection | None = None
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id",
|
||||
values={"asset_id": asset_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_asset(
|
||||
user_id: str,
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Asset | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
model=Asset,
|
||||
)
|
||||
|
||||
|
||||
async def get_public_asset(
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Asset | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||
values={"asset_id": asset_id},
|
||||
model=Asset,
|
||||
)
|
||||
|
||||
|
||||
async def get_public_asset_info(
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||
values={"asset_id": asset_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def update_user_asset_info(
|
||||
asset: AssetInfo,
|
||||
) -> AssetInfo:
|
||||
await db.update("assets", asset)
|
||||
return asset
|
||||
|
||||
|
||||
async def delete_user_asset(
|
||||
user_id: str, asset_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
query="DELETE FROM assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
|
||||
async def get_user_assets(
|
||||
user_id: str,
|
||||
filters: Filters[AssetFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AssetInfo]:
|
||||
filters = filters or Filters()
|
||||
filters.sortby = filters.sortby or "created_at"
|
||||
return await (conn or db).fetch_page(
|
||||
query="SELECT * FROM assets",
|
||||
where=["user_id = :user_id"],
|
||||
values={"user_id": user_id},
|
||||
filters=filters,
|
||||
model=AssetInfo,
|
||||
table_name="assets",
|
||||
)
|
||||
|
||||
|
||||
async def get_user_assets_count(user_id: str) -> int:
|
||||
result = await db.execute(
|
||||
query="SELECT COUNT(*) as count FROM assets WHERE user_id = :user_id",
|
||||
values={"user_id": user_id},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
return row.get("count", 0)
|
||||
@@ -0,0 +1,89 @@
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import AuditEntry, AuditFilters
|
||||
from lnbits.core.models.audit import AuditCountStat
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
|
||||
|
||||
async def create_audit_entry(
|
||||
entry: AuditEntry,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("audit", entry)
|
||||
|
||||
|
||||
async def get_audit_entries(
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AuditEntry]:
|
||||
return await (conn or db).fetch_page(
|
||||
"SELECT * FROM audit",
|
||||
[],
|
||||
{},
|
||||
filters=filters,
|
||||
model=AuditEntry,
|
||||
table_name="audit",
|
||||
)
|
||||
|
||||
|
||||
async def delete_expired_audit_entries(
|
||||
conn: Connection | None = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
DELETE from audit
|
||||
WHERE delete_at < {db.timestamp_now}
|
||||
""", # noqa: S608
|
||||
)
|
||||
|
||||
|
||||
async def get_count_stats(
|
||||
field: str,
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[AuditCountStat]:
|
||||
if field not in ["request_method", "component", "response_code"]:
|
||||
return []
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
clause = filters.where()
|
||||
data = await (conn or db).fetchall(
|
||||
# SQL injection vectors safety:
|
||||
# - `field` is a static string, not user input
|
||||
# - `clause` is generated from filters, which are validated and sanitized
|
||||
query=f"""
|
||||
SELECT {field} as field, count({field}) as total
|
||||
FROM audit
|
||||
{clause}
|
||||
GROUP BY {field}
|
||||
ORDER BY {field}
|
||||
""", # noqa: S608
|
||||
values=filters.values(),
|
||||
model=AuditCountStat,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def get_long_duration_stats(
|
||||
filters: Filters[AuditFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[AuditCountStat]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
clause = filters.where()
|
||||
long_duration_paths = await (conn or db).fetchall(
|
||||
# This query is safe from SQL injection
|
||||
# The `clause` is constructed from sanitized inputs
|
||||
query=f"""
|
||||
SELECT path as field, max(duration) as total FROM audit
|
||||
{clause}
|
||||
GROUP BY path
|
||||
ORDER BY total DESC
|
||||
LIMIT 5
|
||||
""", # noqa: S608
|
||||
values=filters.values(),
|
||||
model=AuditCountStat,
|
||||
)
|
||||
|
||||
return long_duration_paths
|
||||
@@ -0,0 +1,37 @@
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import Connection
|
||||
|
||||
from ..models import DbVersion
|
||||
|
||||
|
||||
async def get_db_version(
|
||||
ext_id: str, conn: Connection | None = None
|
||||
) -> DbVersion | None:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM dbversions WHERE db = :ext_id",
|
||||
{"ext_id": ext_id},
|
||||
model=DbVersion,
|
||||
)
|
||||
|
||||
|
||||
async def get_db_versions(conn: Connection | None = None) -> list[DbVersion]:
|
||||
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
|
||||
|
||||
|
||||
async def update_migration_version(conn, db_name, version):
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO dbversions (db, version) VALUES (:db, :version)
|
||||
ON CONFLICT (db) DO UPDATE SET version = :version
|
||||
""",
|
||||
{"db": db_name, "version": version},
|
||||
)
|
||||
|
||||
|
||||
async def delete_dbversion(*, ext_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE FROM dbversions WHERE db = :ext
|
||||
""",
|
||||
{"ext": ext_id},
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.extensions import (
|
||||
InstallableExtension,
|
||||
UserExtension,
|
||||
)
|
||||
from lnbits.db import Connection, Database
|
||||
|
||||
|
||||
async def create_installed_extension(
|
||||
ext: InstallableExtension,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("installed_extensions", ext)
|
||||
|
||||
|
||||
async def update_installed_extension(
|
||||
ext: InstallableExtension,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).update("installed_extensions", ext)
|
||||
|
||||
|
||||
async def update_installed_extension_state(
|
||||
*, ext_id: str, active: bool, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
UPDATE installed_extensions SET active = :active WHERE id = :ext
|
||||
""",
|
||||
{"ext": ext_id, "active": active},
|
||||
)
|
||||
|
||||
|
||||
async def delete_installed_extension(
|
||||
*, ext_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE from installed_extensions WHERE id = :ext
|
||||
""",
|
||||
{"ext": ext_id},
|
||||
)
|
||||
|
||||
|
||||
async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None:
|
||||
row: dict = await (conn or db).fetchone(
|
||||
"SELECT * FROM dbversions WHERE db = :id",
|
||||
{"id": ext_id},
|
||||
)
|
||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||
if not row:
|
||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||
|
||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||
if is_file_based_db:
|
||||
return
|
||||
|
||||
# String formatting is required, params are not accepted for 'DROP SCHEMA'.
|
||||
# The `ext_id` value is verified above.
|
||||
await (conn or db).execute(
|
||||
f"DROP SCHEMA IF EXISTS {ext_id} CASCADE",
|
||||
)
|
||||
|
||||
|
||||
async def get_installed_extension(
|
||||
ext_id: str, conn: Connection | None = None
|
||||
) -> InstallableExtension | None:
|
||||
extension = await (conn or db).fetchone(
|
||||
"SELECT * FROM installed_extensions WHERE id = :id",
|
||||
{"id": ext_id},
|
||||
InstallableExtension,
|
||||
)
|
||||
return extension
|
||||
|
||||
|
||||
async def get_installed_extensions(
|
||||
active: bool | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[InstallableExtension]:
|
||||
query = "SELECT * FROM installed_extensions"
|
||||
if active is not None:
|
||||
query += " WHERE active = :active"
|
||||
|
||||
values = {"active": active} if active is not None else {}
|
||||
all_extensions = await (conn or db).fetchall(
|
||||
query,
|
||||
values,
|
||||
model=InstallableExtension,
|
||||
)
|
||||
return all_extensions
|
||||
|
||||
|
||||
async def get_installed_extensions_count(conn: Connection | None = None) -> int:
|
||||
row: dict | None = await (conn or db).fetchone(
|
||||
"SELECT COUNT(*) as count FROM installed_extensions"
|
||||
)
|
||||
return int(row["count"]) if row else 0
|
||||
|
||||
|
||||
async def get_user_extension(
|
||||
user_id: str, extension: str, conn: Connection | None = None
|
||||
) -> UserExtension | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM extensions
|
||||
WHERE "user" = :user AND extension = :ext
|
||||
""",
|
||||
{"user": user_id, "ext": extension},
|
||||
model=UserExtension,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_extensions(
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> list[UserExtension]:
|
||||
return await (conn or db).fetchall(
|
||||
"""SELECT * FROM extensions WHERE "user" = :user""",
|
||||
{"user": user_id},
|
||||
model=UserExtension,
|
||||
)
|
||||
|
||||
|
||||
async def create_user_extension(
|
||||
user_extension: UserExtension, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).insert("extensions", user_extension)
|
||||
|
||||
|
||||
async def update_user_extension(
|
||||
user_extension: UserExtension, conn: Connection | None = None
|
||||
) -> None:
|
||||
where = """WHERE extension = :extension AND "user" = :user"""
|
||||
await (conn or db).update("extensions", user_extension, where)
|
||||
|
||||
|
||||
async def get_user_active_extensions_ids(
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> list[str]:
|
||||
exts = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT * FROM extensions WHERE "user" = :user AND active
|
||||
""",
|
||||
{"user": user_id},
|
||||
UserExtension,
|
||||
)
|
||||
return [ext.extension for ext in exts]
|
||||
@@ -0,0 +1,591 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import PaymentState
|
||||
from lnbits.db import Connection, DateTrunc, Filters, Page
|
||||
|
||||
from ..models import (
|
||||
CreatePayment,
|
||||
Payment,
|
||||
PaymentCountField,
|
||||
PaymentCountStat,
|
||||
PaymentDailyStats,
|
||||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
PaymentsStatusCount,
|
||||
PaymentWalletStats,
|
||||
)
|
||||
|
||||
|
||||
def update_payment_extra():
|
||||
pass
|
||||
|
||||
|
||||
async def get_payment(checking_id: str, conn: Connection | None = None) -> Payment:
|
||||
return await (conn or db).fetchone(
|
||||
"SELECT * FROM apipayments WHERE checking_id = :checking_id",
|
||||
{"checking_id": checking_id},
|
||||
Payment,
|
||||
)
|
||||
|
||||
|
||||
async def get_standalone_payment(
|
||||
checking_id_or_hash: str,
|
||||
incoming: bool | None = False,
|
||||
wallet_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment | None:
|
||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||
values = {
|
||||
"checking_id": checking_id_or_hash,
|
||||
"hash": checking_id_or_hash,
|
||||
}
|
||||
if incoming:
|
||||
clause = f"({clause}) AND amount > 0"
|
||||
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
# This query is safe from SQL injection
|
||||
# The `clause` is constructed from sanitized inputs
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE {clause}
|
||||
ORDER BY amount LIMIT 1
|
||||
""", # noqa: S608
|
||||
values,
|
||||
Payment,
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
async def get_wallet_payment(
|
||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
payment = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE wallet_id = :wallet AND payment_hash = :hash
|
||||
""",
|
||||
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
|
||||
Payment,
|
||||
)
|
||||
return payment
|
||||
|
||||
|
||||
async def get_latest_payments_by_extension(
|
||||
ext_name: str, ext_id: str, limit: int = 5
|
||||
) -> list[Payment]:
|
||||
return await db.fetchall(
|
||||
# This query is safe from SQL injection
|
||||
# The limtit is an integer and not user input
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE status = :status
|
||||
AND extra LIKE :ext_name
|
||||
AND extra LIKE :ext_id
|
||||
ORDER BY time DESC LIMIT {int(limit)}
|
||||
""", # noqa: S608
|
||||
{
|
||||
"status": f"{PaymentState.SUCCESS}",
|
||||
"ext_name": f"%{ext_name}%",
|
||||
"ext_id": f"%{ext_id}%",
|
||||
},
|
||||
Payment,
|
||||
)
|
||||
|
||||
|
||||
async def get_payments_paginated( # noqa: C901
|
||||
*,
|
||||
wallet_id: str | None = None,
|
||||
user_id: str | None = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
failed: bool = False,
|
||||
outgoing: bool = False,
|
||||
incoming: bool = False,
|
||||
since: int | None = None,
|
||||
exclude_uncheckable: bool = False,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by:
|
||||
- complete | pending | failed | outgoing | incoming.
|
||||
"""
|
||||
values: dict[str, Any] = {
|
||||
"time": since,
|
||||
}
|
||||
clause: list[str] = []
|
||||
|
||||
if since is not None:
|
||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return Page(data=[], total=0)
|
||||
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
clause.append("wallet_id = :wallet_id")
|
||||
elif user_id:
|
||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||
clause.append(only_user_wallets)
|
||||
|
||||
if complete and pending:
|
||||
clause.append(
|
||||
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||
)
|
||||
elif complete:
|
||||
clause.append(f"""
|
||||
(
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
""")
|
||||
elif pending:
|
||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||
elif failed:
|
||||
clause.append(f"status = '{PaymentState.FAILED}'")
|
||||
|
||||
if outgoing and incoming:
|
||||
pass
|
||||
elif outgoing:
|
||||
clause.append("amount < 0")
|
||||
elif incoming:
|
||||
clause.append("amount > 0")
|
||||
else:
|
||||
pass
|
||||
|
||||
if exclude_uncheckable: # checkable means it has a checking_id that isn't internal
|
||||
clause.append("checking_id NOT LIKE 'temp_%'")
|
||||
clause.append("checking_id NOT LIKE 'internal_%'")
|
||||
|
||||
return await (conn or db).fetch_page(
|
||||
"SELECT * FROM apipayments",
|
||||
clause,
|
||||
values,
|
||||
filters=filters,
|
||||
model=Payment,
|
||||
table_name="apipayments",
|
||||
)
|
||||
|
||||
|
||||
async def get_payments(
|
||||
*,
|
||||
wallet_id: str | None = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
outgoing: bool = False,
|
||||
incoming: bool = False,
|
||||
since: int | None = None,
|
||||
exclude_uncheckable: bool = False,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||
"""
|
||||
|
||||
filters = filters or Filters()
|
||||
|
||||
filters.sortby = filters.sortby or "time"
|
||||
filters.direction = filters.direction or "desc"
|
||||
filters.limit = limit or filters.limit
|
||||
filters.offset = offset or filters.offset
|
||||
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet_id,
|
||||
complete=complete,
|
||||
pending=pending,
|
||||
outgoing=outgoing,
|
||||
incoming=incoming,
|
||||
since=since,
|
||||
exclude_uncheckable=exclude_uncheckable,
|
||||
filters=filters,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
return page.data
|
||||
|
||||
|
||||
async def get_payments_status_count() -> PaymentsStatusCount:
|
||||
empty_page: Filters = Filters(limit=0)
|
||||
in_payments = await get_payments_paginated(incoming=True, filters=empty_page)
|
||||
out_payments = await get_payments_paginated(outgoing=True, filters=empty_page)
|
||||
pending_payments = await get_payments_paginated(pending=True, filters=empty_page)
|
||||
failed_payments = await get_payments_paginated(failed=True, filters=empty_page)
|
||||
|
||||
return PaymentsStatusCount(
|
||||
incoming=in_payments.total,
|
||||
outgoing=out_payments.total,
|
||||
pending=pending_payments.total,
|
||||
failed=failed_payments.total,
|
||||
)
|
||||
|
||||
|
||||
async def delete_expired_invoices(
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
# first we delete all invoices older than one month
|
||||
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
DELETE FROM apipayments
|
||||
WHERE status = :status AND amount > 0
|
||||
AND time < {db.timestamp_placeholder("delta")}
|
||||
""", # noqa: S608
|
||||
{"status": f"{PaymentState.PENDING}", "delta": int(time() - 2592000)},
|
||||
)
|
||||
# then we delete all invoices whose expiry date is in the past
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
DELETE FROM apipayments
|
||||
WHERE status = :status AND amount > 0
|
||||
AND expiry < {db.timestamp_placeholder("now")}
|
||||
""", # noqa: S608
|
||||
{"status": f"{PaymentState.PENDING}", "now": int(time())},
|
||||
)
|
||||
|
||||
|
||||
async def create_payment(
|
||||
checking_id: str,
|
||||
data: CreatePayment,
|
||||
status: PaymentState = PaymentState.PENDING,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
# we don't allow the creation of the same invoice twice
|
||||
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||
previous_payment = await get_standalone_payment(checking_id, conn=conn)
|
||||
if previous_payment is not None:
|
||||
raise ValueError("Payment already exists")
|
||||
extra = data.extra or {}
|
||||
|
||||
payment = Payment(
|
||||
checking_id=checking_id,
|
||||
status=status,
|
||||
wallet_id=data.wallet_id,
|
||||
payment_hash=data.payment_hash,
|
||||
bolt11=data.bolt11,
|
||||
amount=data.amount_msat,
|
||||
memo=data.memo,
|
||||
preimage=data.preimage,
|
||||
expiry=data.expiry,
|
||||
webhook=data.webhook,
|
||||
fee=-abs(data.fee),
|
||||
tag=extra.get("tag", None),
|
||||
extra=extra,
|
||||
labels=data.labels or [],
|
||||
)
|
||||
|
||||
await (conn or db).insert("apipayments", payment)
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
async def update_payment_checking_id(
|
||||
checking_id: str, new_checking_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
UPDATE apipayments
|
||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
||||
WHERE checking_id = :old_id
|
||||
""", # noqa: S608
|
||||
{
|
||||
"new_id": new_checking_id,
|
||||
"old_id": checking_id,
|
||||
"now": int(time()),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def update_payment(
|
||||
payment: Payment,
|
||||
new_checking_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
payment.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update(
|
||||
"apipayments", payment, "WHERE checking_id = :checking_id"
|
||||
)
|
||||
if new_checking_id and new_checking_id != payment.checking_id:
|
||||
await update_payment_checking_id(payment.checking_id, new_checking_id, conn)
|
||||
|
||||
|
||||
async def get_payments_history(
|
||||
wallet_id: str | None = None,
|
||||
group: DateTrunc = "day",
|
||||
filters: Filters | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentHistoryPoint]:
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
date_trunc = db.datetime_grouping(group)
|
||||
|
||||
values: dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
}
|
||||
# count outgoing payments if they are still pending
|
||||
where = [f"""
|
||||
wallet_id = :wallet_id AND (
|
||||
status = '{PaymentState.SUCCESS}'
|
||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||
)
|
||||
"""]
|
||||
clause = filters.where(where)
|
||||
transactions: list[dict] = await db.fetchall(
|
||||
# This query is safe from SQL injection:
|
||||
# - `date_trunc` is a static string, not user input
|
||||
# - `clause` is generated from filters, which are validated and sanitized
|
||||
f"""
|
||||
SELECT {date_trunc} date,
|
||||
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) income,
|
||||
SUM(CASE WHEN amount < 0 THEN abs(amount) + abs(fee) ELSE 0 END) spending
|
||||
FROM apipayments
|
||||
{clause}
|
||||
GROUP BY date
|
||||
ORDER BY date DESC
|
||||
""", # noqa: S608
|
||||
filters.values(values),
|
||||
)
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return []
|
||||
balance = wallet.balance_msat
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
else:
|
||||
balance = await get_total_balance(conn=conn)
|
||||
|
||||
# since we dont know the balance at the starting point,
|
||||
# we take the current balance and walk backwards
|
||||
results: list[PaymentHistoryPoint] = []
|
||||
for row in transactions:
|
||||
results.insert(
|
||||
0,
|
||||
PaymentHistoryPoint(
|
||||
balance=balance,
|
||||
date=row.get("date", 0),
|
||||
income=row.get("income", 0),
|
||||
spending=row.get("spending", 0),
|
||||
),
|
||||
)
|
||||
balance -= row.get("income", 0) - row.get("spending", 0)
|
||||
return results
|
||||
|
||||
|
||||
async def get_payment_count_stats(
|
||||
field: PaymentCountField,
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentCountStat]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
extra_stmts = []
|
||||
|
||||
if user_id:
|
||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||
extra_stmts.append(only_user_wallets)
|
||||
|
||||
clause = filters.where(extra_stmts)
|
||||
data = await (conn or db).fetchall(
|
||||
# SQL injection vectors safety:
|
||||
# - `field` is a static string, not user input
|
||||
# - `clause` is generated from filters, which are validated and sanitized
|
||||
query=f"""
|
||||
SELECT {field} as field, count(*) as total
|
||||
FROM apipayments
|
||||
{clause}
|
||||
GROUP BY {field}
|
||||
ORDER BY {field}
|
||||
""", # noqa: S608
|
||||
values=filters.values(),
|
||||
model=PaymentCountStat,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def get_daily_stats(
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
in_where_stmts = ["(apipayments.status = 'success' AND apipayments.amount > 0)"]
|
||||
out_where_stmts = [
|
||||
"(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"
|
||||
]
|
||||
|
||||
if user_id:
|
||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||
in_where_stmts.append(only_user_wallets)
|
||||
out_where_stmts.append(only_user_wallets)
|
||||
|
||||
in_clause = filters.where(in_where_stmts)
|
||||
out_clause = filters.where(out_where_stmts)
|
||||
|
||||
date_trunc = db.datetime_grouping("day")
|
||||
query = """
|
||||
SELECT {date_trunc} date,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance,
|
||||
ABS(SUM(apipayments.fee)) as fee,
|
||||
COUNT(*) as payments_count
|
||||
FROM wallets
|
||||
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
|
||||
{clause}
|
||||
AND (wallets.deleted = false OR wallets.deleted is NULL)
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
"""
|
||||
|
||||
data_in = await (conn or db).fetchall(
|
||||
query=query.format(date_trunc=date_trunc, clause=in_clause),
|
||||
values=filters.values(),
|
||||
model=PaymentDailyStats,
|
||||
)
|
||||
data_out = await (conn or db).fetchall(
|
||||
query=query.format(date_trunc=date_trunc, clause=out_clause),
|
||||
values=filters.values(),
|
||||
model=PaymentDailyStats,
|
||||
)
|
||||
|
||||
return data_in, data_out
|
||||
|
||||
|
||||
async def get_wallets_stats(
|
||||
filters: Filters[PaymentFilters] | None = None,
|
||||
user_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[PaymentWalletStats]:
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
where_stmts = [
|
||||
"(wallets.deleted = false OR wallets.deleted is NULL)",
|
||||
"""
|
||||
(
|
||||
(apipayments.status = 'success' AND apipayments.amount > 0)
|
||||
OR (
|
||||
apipayments.status IN ('success', 'pending')
|
||||
AND apipayments.amount < 0
|
||||
)
|
||||
)
|
||||
""",
|
||||
]
|
||||
if user_id:
|
||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||
where_stmts.append(only_user_wallets)
|
||||
|
||||
clauses = filters.where(where_stmts)
|
||||
|
||||
data = await (conn or db).fetchall(
|
||||
# This query is safe from SQL injection
|
||||
# The `clauses` is constructed from sanitized inputs
|
||||
query=f"""
|
||||
SELECT apipayments.wallet_id,
|
||||
MAX(wallets.name) AS wallet_name,
|
||||
MAX(wallets.user) AS user_id,
|
||||
COUNT(*) as payments_count,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
FROM wallets
|
||||
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
|
||||
{clauses}
|
||||
GROUP BY apipayments.wallet_id
|
||||
ORDER BY payments_count
|
||||
""", # noqa: S608
|
||||
values=filters.values(),
|
||||
model=PaymentWalletStats,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def delete_wallet_payment(
|
||||
checking_id: str, wallet_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
|
||||
{"checking_id": checking_id, "wallet": wallet_id},
|
||||
)
|
||||
|
||||
|
||||
async def check_internal(
|
||||
payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
"""
|
||||
Returns the checking_id of the internal payment if it exists,
|
||||
otherwise None
|
||||
"""
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE payment_hash = :hash AND status = :status AND amount > 0
|
||||
""",
|
||||
{"status": f"{PaymentState.PENDING}", "hash": payment_hash},
|
||||
Payment,
|
||||
)
|
||||
|
||||
|
||||
async def is_internal_status_success(
|
||||
payment_hash: str, conn: Connection | None = None
|
||||
) -> bool:
|
||||
"""
|
||||
Returns True if the internal payment was found and is successful,
|
||||
"""
|
||||
payment = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE payment_hash = :payment_hash AND amount > 0
|
||||
""",
|
||||
{"payment_hash": payment_hash},
|
||||
Payment,
|
||||
)
|
||||
if not payment:
|
||||
return False
|
||||
return payment.status == PaymentState.SUCCESS.value
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = :status
|
||||
WHERE payment_hash = :hash
|
||||
""",
|
||||
{"status": status, "hash": payment_hash},
|
||||
)
|
||||
|
||||
|
||||
async def _only_user_wallets_statement(
|
||||
user_id: str, conn: Connection | None = None
|
||||
) -> str:
|
||||
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||
"no-wallets-for-user"
|
||||
]
|
||||
# wallet ids are safe to use in sql queries
|
||||
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
|
||||
return f""" wallet_id IN ({", ".join(wallet_ids_str)}) """
|
||||
@@ -0,0 +1,139 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import dict_to_model
|
||||
from lnbits.settings import (
|
||||
AdminSettings,
|
||||
EditableSettings,
|
||||
SettingsField,
|
||||
SuperSettings,
|
||||
settings,
|
||||
)
|
||||
|
||||
|
||||
async def get_super_settings() -> SuperSettings | None:
|
||||
data = await get_settings_by_tag("core")
|
||||
if data:
|
||||
super_user = await get_settings_field("super_user")
|
||||
super_user_id = super_user.value if super_user else None
|
||||
settings_dict = {"super_user": super_user_id, **data}
|
||||
return dict_to_model(settings_dict, SuperSettings)
|
||||
return None
|
||||
|
||||
|
||||
async def get_admin_settings(is_super_user: bool = False) -> AdminSettings | None:
|
||||
sets = await get_super_settings()
|
||||
if not sets:
|
||||
return None
|
||||
row_dict = dict(sets)
|
||||
row_dict.pop("super_user")
|
||||
row_dict.pop("auth_all_methods")
|
||||
|
||||
admin_settings = AdminSettings(
|
||||
is_super_user=is_super_user,
|
||||
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
|
||||
**row_dict,
|
||||
)
|
||||
return admin_settings
|
||||
|
||||
|
||||
async def update_admin_settings(
|
||||
data: EditableSettings, tag: str | None = "core"
|
||||
) -> None:
|
||||
editable_settings = await get_settings_by_tag("core") or {}
|
||||
editable_settings.update(data.dict(exclude_unset=True))
|
||||
for key, value in editable_settings.items():
|
||||
try:
|
||||
await set_settings_field(key, value, tag)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
logger.warning(f"Failed to update settings for '{tag}.{key}'.")
|
||||
|
||||
|
||||
async def update_super_user(super_user: str) -> SuperSettings:
|
||||
await set_settings_field("super_user", super_user)
|
||||
settings = await get_super_settings()
|
||||
if not settings:
|
||||
raise ValueError("updated super_user settings could not be retrieved")
|
||||
return settings
|
||||
|
||||
|
||||
async def delete_admin_settings(tag: str | None = "core") -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM system_settings WHERE tag = :tag",
|
||||
{"tag": tag},
|
||||
)
|
||||
|
||||
|
||||
async def reset_core_settings() -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
DELETE FROM system_settings WHERE tag = 'core'
|
||||
AND id NOT IN (
|
||||
'super_user',
|
||||
'lnbits_webpush_pubkey',
|
||||
'lnbits_webpush_privkey'
|
||||
)
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
||||
data = {"super_user": super_user, **new_settings}
|
||||
for key, value in data.items():
|
||||
await set_settings_field(key, value)
|
||||
|
||||
settings = await get_super_settings()
|
||||
if not settings:
|
||||
raise ValueError("created admin settings could not be retrieved")
|
||||
return settings
|
||||
|
||||
|
||||
async def get_settings_field(
|
||||
id_: str, tag: str | None = "core"
|
||||
) -> SettingsField | None:
|
||||
|
||||
row: dict = await db.fetchone(
|
||||
"""
|
||||
SELECT * FROM system_settings
|
||||
WHERE id = :id AND tag = :tag
|
||||
""",
|
||||
{"id": id_, "tag": tag},
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
value = json.loads(row["value"]) if row["value"] else None
|
||||
return SettingsField(id=row["id"], value=value, tag=row["tag"])
|
||||
|
||||
|
||||
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
||||
value = json.dumps(value) if value is not None else None
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO system_settings (id, value, tag)
|
||||
VALUES (:id, :value, :tag)
|
||||
ON CONFLICT (id, tag) DO UPDATE SET value = :value
|
||||
""",
|
||||
{"id": id_, "value": value, "tag": tag or "core"},
|
||||
)
|
||||
|
||||
|
||||
async def get_settings_by_tag(tag: str) -> dict[str, Any] | None:
|
||||
rows: list[dict] = await db.fetchall(
|
||||
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
|
||||
)
|
||||
if len(rows) == 0:
|
||||
return None
|
||||
data: dict[str, Any] = {}
|
||||
for row in rows:
|
||||
try:
|
||||
data[row["id"]] = json.loads(row["value"]) if row["value"] else None
|
||||
except Exception as _:
|
||||
logger.warning(
|
||||
f"""Failed to load settings value for '{tag}.{row["id"]}'."""
|
||||
)
|
||||
data.pop("super_user")
|
||||
return data
|
||||
@@ -0,0 +1,40 @@
|
||||
import shortuuid
|
||||
|
||||
from lnbits.core.db import db
|
||||
|
||||
from ..models import TinyURL
|
||||
|
||||
|
||||
async def create_tinyurl(domain: str, endless: bool, wallet: str):
|
||||
tinyurl_id = shortuuid.uuid()[:8]
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO tiny_url (id, url, endless, wallet)
|
||||
VALUES (:tinyurl, :domain, :endless, :wallet)
|
||||
""",
|
||||
{"tinyurl": tinyurl_id, "domain": domain, "endless": endless, "wallet": wallet},
|
||||
)
|
||||
return await get_tinyurl(tinyurl_id)
|
||||
|
||||
|
||||
async def get_tinyurl(tinyurl_id: str) -> TinyURL | None:
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM tiny_url WHERE id = :tinyurl",
|
||||
{"tinyurl": tinyurl_id},
|
||||
TinyURL,
|
||||
)
|
||||
|
||||
|
||||
async def get_tinyurl_by_url(url: str) -> list[TinyURL]:
|
||||
return await db.fetchall(
|
||||
"SELECT * FROM tiny_url WHERE url = :url",
|
||||
{"url": url},
|
||||
TinyURL,
|
||||
)
|
||||
|
||||
|
||||
async def delete_tinyurl(tinyurl_id: str):
|
||||
await db.execute(
|
||||
"DELETE FROM tiny_url WHERE id = :tinyurl",
|
||||
{"tinyurl": tinyurl_id},
|
||||
)
|
||||
@@ -0,0 +1,298 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import (
|
||||
clear_wallet_cache,
|
||||
create_wallet,
|
||||
get_standalone_wallet,
|
||||
get_wallets,
|
||||
)
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.helpers import sha256s
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import (
|
||||
Account,
|
||||
AccountFilters,
|
||||
AccountOverview,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
async def create_account(
|
||||
account: Account | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Account:
|
||||
if account:
|
||||
account.validate_fields()
|
||||
else:
|
||||
now = datetime.now(timezone.utc)
|
||||
account = Account(id=uuid4().hex, created_at=now, updated_at=now)
|
||||
await (conn or db).insert("accounts", account)
|
||||
return account
|
||||
|
||||
|
||||
async def get_accounts_count(conn: Connection | None = None) -> int:
|
||||
row: dict | None = await (conn or db).fetchone(
|
||||
"SELECT COUNT(*) as count FROM accounts"
|
||||
)
|
||||
return int(row["count"]) if row else 0
|
||||
|
||||
|
||||
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("accounts", account)
|
||||
return account
|
||||
|
||||
|
||||
async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE from accounts WHERE id = :user",
|
||||
{"user": user_id},
|
||||
)
|
||||
await clear_user_id_cache(user_id)
|
||||
|
||||
|
||||
async def get_accounts(
|
||||
filters: Filters[AccountFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AccountOverview]:
|
||||
where_clauses = []
|
||||
values: dict[str, Any] = {}
|
||||
filters = filters or Filters()
|
||||
|
||||
wallet_filter = filters.get_filter_by_field("wallet_id")
|
||||
|
||||
if wallet_filter and wallet_filter.values:
|
||||
wallet_id_value = next(iter(wallet_filter.values.values()), None)
|
||||
wallet = (
|
||||
await get_standalone_wallet(wallet_id_value, deleted=None, conn=conn)
|
||||
if wallet_id_value
|
||||
else None
|
||||
)
|
||||
if not wallet:
|
||||
return Page(data=[], total=0)
|
||||
where_clauses.append("accounts.id = :account_id")
|
||||
values = {**values, "account_id": wallet.user}
|
||||
filters.remove_filter_by_field("wallet_id")
|
||||
|
||||
return await (conn or db).fetch_page(
|
||||
"""
|
||||
SELECT
|
||||
accounts.id,
|
||||
accounts.username,
|
||||
accounts.email,
|
||||
accounts.pubkey,
|
||||
accounts.external_id,
|
||||
accounts.activated,
|
||||
SUM(COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)) as balance_msat,
|
||||
SUM((
|
||||
SELECT COUNT(*) FROM apipayments WHERE wallet_id = wallets.id
|
||||
)) as transaction_count,
|
||||
(
|
||||
SELECT COUNT(*) FROM wallets WHERE wallets.user = accounts.id
|
||||
) as wallet_count,
|
||||
MAX((
|
||||
SELECT time FROM apipayments
|
||||
WHERE wallet_id = wallets.id ORDER BY time DESC LIMIT 1
|
||||
)) as last_payment
|
||||
FROM accounts LEFT JOIN wallets ON accounts.id = wallets.user
|
||||
""",
|
||||
where_clauses,
|
||||
values,
|
||||
filters=filters,
|
||||
model=AccountOverview,
|
||||
group_by=["accounts.id"],
|
||||
table_name="accounts",
|
||||
)
|
||||
|
||||
|
||||
async def get_account(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(user_id) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"id": user_id, "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def delete_accounts_no_wallets(
|
||||
time_delta: int,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
delta = int(time()) - time_delta
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
DELETE FROM accounts
|
||||
WHERE NOT EXISTS (
|
||||
SELECT wallets.id FROM wallets WHERE wallets.user = accounts.id
|
||||
) AND (
|
||||
(updated_at is null AND created_at < :delta)
|
||||
OR updated_at < {db.timestamp_placeholder("delta")}
|
||||
)
|
||||
""", # noqa: S608
|
||||
{"delta": delta},
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username(
|
||||
username: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(username) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(username) = :username
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"username": username.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_pubkey(
|
||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(pubkey) = :pubkey
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_email(
|
||||
email: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> Account | None:
|
||||
if len(email) == 0:
|
||||
return None
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
LOWER(email) = :email
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"email": email.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_account_by_username_or_email(
|
||||
username_or_email: str,
|
||||
active_only: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> Account | None:
|
||||
|
||||
return await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * FROM accounts
|
||||
WHERE
|
||||
(LOWER(email) = :value or LOWER(username) = :value)
|
||||
AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"value": username_or_email.lower(), "activated": active_only},
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
async def get_user(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
account = await get_account(user_id, active_only, conn=conn)
|
||||
if not account:
|
||||
return None
|
||||
return await get_user_from_account(account, conn=conn)
|
||||
|
||||
|
||||
async def get_user_from_account(
|
||||
account: Account, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
||||
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
||||
|
||||
if len(wallets) == 0:
|
||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||
wallets.append(wallet)
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
activated=account.activated,
|
||||
email=account.email,
|
||||
username=account.username,
|
||||
pubkey=account.pubkey,
|
||||
external_id=account.external_id,
|
||||
extra=account.extra,
|
||||
created_at=account.created_at,
|
||||
updated_at=account.updated_at,
|
||||
extensions=extensions,
|
||||
wallets=wallets,
|
||||
admin=account.is_admin,
|
||||
super_user=account.is_super_user,
|
||||
fiat_providers=account.fiat_providers,
|
||||
has_password=account.password_hash is not None,
|
||||
ui_customization=account.ui_customization or {},
|
||||
)
|
||||
|
||||
|
||||
async def update_user_access_control_list(
|
||||
user_acls: UserAcls, conn: Connection | None = None
|
||||
):
|
||||
user_acls.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("accounts", user_acls)
|
||||
|
||||
|
||||
async def get_user_access_control_lists(
|
||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
||||
) -> UserAcls:
|
||||
user_acls = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT id, access_control_list FROM accounts
|
||||
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
||||
""",
|
||||
{"user_id": user_id, "activated": active_only},
|
||||
UserAcls,
|
||||
)
|
||||
|
||||
return user_acls or UserAcls(id=user_id)
|
||||
|
||||
|
||||
async def clear_user_id_cache(user_id: str):
|
||||
user = await get_user(user_id, active_only=True)
|
||||
if user:
|
||||
clear_user_cache(user)
|
||||
|
||||
|
||||
def clear_user_cache(user: User):
|
||||
user_cache_key: str | None = cache.pop(
|
||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||
)
|
||||
if user_cache_key:
|
||||
cache.pop(user_cache_key)
|
||||
for wallet in user.wallets:
|
||||
clear_wallet_cache(wallet)
|
||||
@@ -0,0 +1,313 @@
|
||||
from datetime import datetime, timezone
|
||||
from time import time
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.wallets import BaseWallet, WalletsFilters, WalletType
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.cache import cache
|
||||
|
||||
from ..models import Wallet
|
||||
|
||||
|
||||
async def create_wallet(
|
||||
*,
|
||||
user_id: str,
|
||||
wallet_name: str | None = None,
|
||||
wallet_type: WalletType = WalletType.LIGHTNING,
|
||||
shared_wallet_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
wallet_id = uuid4().hex
|
||||
wallet = Wallet(
|
||||
id=wallet_id,
|
||||
name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
wallet_type=wallet_type.value,
|
||||
shared_wallet_id=shared_wallet_id,
|
||||
user=user_id,
|
||||
adminkey=uuid4().hex,
|
||||
inkey=uuid4().hex,
|
||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||
)
|
||||
|
||||
await (conn or db).insert("wallets", wallet)
|
||||
return wallet
|
||||
|
||||
|
||||
async def update_wallet(
|
||||
wallet: Wallet,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
wallet.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("wallets", wallet)
|
||||
return wallet
|
||||
|
||||
|
||||
async def delete_wallet(
|
||||
user_id: str,
|
||||
wallet_id: str,
|
||||
deleted: bool = True,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
now = int(time())
|
||||
|
||||
await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE wallets
|
||||
SET deleted = :deleted, updated_at = {db.timestamp_placeholder('now')}
|
||||
WHERE id = :wallet AND "user" = :user
|
||||
""", # noqa: S608
|
||||
{"wallet": wallet_id, "user": user_id, "deleted": deleted, "now": now},
|
||||
)
|
||||
|
||||
|
||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM wallets WHERE id = :wallet",
|
||||
{"wallet": wallet_id},
|
||||
)
|
||||
|
||||
|
||||
async def delete_wallet_by_id(
|
||||
wallet_id: str, conn: Connection | None = None
|
||||
) -> int | None:
|
||||
clear_wallet_id_cache(wallet_id)
|
||||
now = int(time())
|
||||
result = await (conn or db).execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE wallets
|
||||
SET deleted = true, updated_at = {db.timestamp_placeholder('now')}
|
||||
WHERE id = :wallet
|
||||
""", # noqa: S608
|
||||
{"wallet": wallet_id, "now": now},
|
||||
)
|
||||
return result.rowcount
|
||||
|
||||
|
||||
async def remove_deleted_wallets(conn: Connection | None = None) -> None:
|
||||
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
|
||||
|
||||
|
||||
async def delete_unused_wallets(
|
||||
time_delta: int,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
delta = int(time()) - time_delta
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE FROM wallets
|
||||
WHERE (
|
||||
SELECT COUNT(*) FROM apipayments WHERE wallet_id = wallets.id
|
||||
) = 0 AND (
|
||||
(updated_at is null AND created_at < :delta)
|
||||
OR updated_at < :delta
|
||||
)
|
||||
""",
|
||||
{"delta": delta},
|
||||
)
|
||||
|
||||
|
||||
async def get_standalone_wallet(
|
||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0) AS balance_msat FROM wallets
|
||||
WHERE id = :wallet
|
||||
"""
|
||||
if deleted is not None:
|
||||
query += " AND deleted = :deleted "
|
||||
return await (conn or db).fetchone(
|
||||
query,
|
||||
{"wallet": wallet_id, "deleted": deleted},
|
||||
Wallet,
|
||||
)
|
||||
|
||||
|
||||
async def get_wallet(
|
||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
wallet = await get_standalone_wallet(wallet_id, deleted, conn)
|
||||
if not wallet:
|
||||
return None
|
||||
if wallet.is_lightning_shared_wallet:
|
||||
return await get_source_wallet(wallet, conn)
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_wallets(
|
||||
user_id: str,
|
||||
deleted: bool | None = False,
|
||||
wallet_type: WalletType | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[Wallet]:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0) AS balance_msat FROM wallets
|
||||
WHERE "user" = :user
|
||||
"""
|
||||
if deleted is not None:
|
||||
query += " AND deleted = :deleted "
|
||||
if wallet_type is not None:
|
||||
query += " AND wallet_type = :wallet_type "
|
||||
wallets = await (conn or db).fetchall(
|
||||
query,
|
||||
{
|
||||
"user": user_id,
|
||||
"deleted": deleted,
|
||||
"wallet_type": wallet_type.value if wallet_type else None,
|
||||
},
|
||||
Wallet,
|
||||
)
|
||||
|
||||
return await get_source_wallets(wallets, conn)
|
||||
|
||||
|
||||
async def get_wallets_paginated(
|
||||
user_id: str,
|
||||
deleted: bool | None = None,
|
||||
filters: Filters[WalletsFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[Wallet]:
|
||||
if deleted is None:
|
||||
deleted = False
|
||||
|
||||
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||
wallets = await (conn or db).fetch_page(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0) AS balance_msat FROM wallets
|
||||
""",
|
||||
where=where,
|
||||
values={"user": user_id, "deleted": deleted},
|
||||
filters=filters,
|
||||
model=Wallet,
|
||||
table_name="wallets",
|
||||
)
|
||||
|
||||
wallets.data = await get_source_wallets(wallets.data, conn)
|
||||
return wallets
|
||||
|
||||
|
||||
async def get_wallets_ids(
|
||||
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> list[str]:
|
||||
query = """SELECT * FROM wallets WHERE "user" = :user"""
|
||||
if deleted is not None:
|
||||
query += " AND deleted = :deleted "
|
||||
wallets = await (conn or db).fetchall(
|
||||
query,
|
||||
{"user": user_id, "deleted": deleted},
|
||||
Wallet,
|
||||
)
|
||||
|
||||
wallets = await get_source_wallets(wallets, conn)
|
||||
return [w.source_wallet_id for w in wallets if w.can_view_payments]
|
||||
|
||||
|
||||
async def get_wallets_count():
|
||||
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
|
||||
row = result.mappings().first()
|
||||
return row.get("count", 0)
|
||||
|
||||
|
||||
async def get_wallet_for_key(
|
||||
key: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT wallets.*, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
), 0)
|
||||
AS balance_msat FROM wallets
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
""",
|
||||
{"key": key},
|
||||
Wallet,
|
||||
)
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
if wallet.is_lightning_shared_wallet:
|
||||
mw = await get_source_wallet(wallet, conn)
|
||||
return mw
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_base_wallet_for_key(
|
||||
key: str,
|
||||
conn: Connection | None = None,
|
||||
) -> BaseWallet | None:
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||
INNER JOIN accounts ON wallets.user = accounts.id
|
||||
WHERE (adminkey = :key OR inkey = :key)
|
||||
AND deleted = false
|
||||
AND accounts.activated = true
|
||||
""",
|
||||
{"key": key},
|
||||
BaseWallet,
|
||||
)
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_source_wallet(
|
||||
wallet: Wallet, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
if not wallet.is_lightning_shared_wallet:
|
||||
return wallet
|
||||
if not wallet.shared_wallet_id:
|
||||
return None
|
||||
|
||||
shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
|
||||
if not shared_wallet:
|
||||
return None
|
||||
wallet.mirror_shared_wallet(shared_wallet)
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_source_wallets(
|
||||
wallet: list[Wallet], conn: Connection | None = None
|
||||
) -> list[Wallet]:
|
||||
source_wallets = []
|
||||
for w in wallet:
|
||||
source_wallet = await get_source_wallet(w, conn)
|
||||
if source_wallet:
|
||||
source_wallets.append(source_wallet)
|
||||
return source_wallets
|
||||
|
||||
|
||||
async def get_total_balance(conn: Connection | None = None):
|
||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||
row = result.mappings().first()
|
||||
return row.get("balance", 0) or 0
|
||||
|
||||
|
||||
def clear_wallet_id_cache(wallet_id: str):
|
||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||
if cached_wallet:
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||
|
||||
|
||||
def clear_wallet_cache(wallet: Wallet):
|
||||
cache.pop(f"auth:wallet:{wallet.id}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||
@@ -0,0 +1,58 @@
|
||||
from lnbits.core.db import db
|
||||
|
||||
from ..models import WebPushSubscription
|
||||
|
||||
|
||||
async def get_webpush_subscription(
|
||||
endpoint: str, user: str
|
||||
) -> WebPushSubscription | None:
|
||||
return await db.fetchone(
|
||||
"""
|
||||
SELECT * FROM webpush_subscriptions
|
||||
WHERE endpoint = :endpoint AND "user" = :user
|
||||
""",
|
||||
{"endpoint": endpoint, "user": user},
|
||||
WebPushSubscription,
|
||||
)
|
||||
|
||||
|
||||
async def get_webpush_subscriptions_for_user(user: str) -> list[WebPushSubscription]:
|
||||
return await db.fetchall(
|
||||
"""SELECT * FROM webpush_subscriptions WHERE "user" = :user""",
|
||||
{"user": user},
|
||||
WebPushSubscription,
|
||||
)
|
||||
|
||||
|
||||
async def create_webpush_subscription(
|
||||
endpoint: str, user: str, data: str, host: str
|
||||
) -> WebPushSubscription:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO webpush_subscriptions (endpoint, "user", data, host)
|
||||
VALUES (:endpoint, :user, :data, :host)
|
||||
""",
|
||||
{"endpoint": endpoint, "user": user, "data": data, "host": host},
|
||||
)
|
||||
subscription = await get_webpush_subscription(endpoint, user)
|
||||
if not subscription:
|
||||
raise ValueError("Newly created webpush subscription couldn't be retrieved")
|
||||
return subscription
|
||||
|
||||
|
||||
async def delete_webpush_subscription(endpoint: str, user: str) -> int:
|
||||
resp = await db.execute(
|
||||
"""
|
||||
DELETE FROM webpush_subscriptions WHERE endpoint = :endpoint AND "user" = :user
|
||||
""",
|
||||
{"endpoint": endpoint, "user": user},
|
||||
)
|
||||
return resp.rowcount
|
||||
|
||||
|
||||
async def delete_webpush_subscriptions(endpoint: str) -> int:
|
||||
resp = await db.execute(
|
||||
"DELETE FROM webpush_subscriptions WHERE endpoint = :endpoint",
|
||||
{"endpoint": endpoint},
|
||||
)
|
||||
return resp.rowcount
|
||||
@@ -1,42 +1,52 @@
|
||||
import importlib
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import migrations as core_migrations
|
||||
from lnbits.core.crud import (
|
||||
get_db_versions,
|
||||
get_installed_extensions,
|
||||
update_migration_version,
|
||||
)
|
||||
from lnbits.core.db import db as core_db
|
||||
from lnbits.db import Connection
|
||||
from lnbits.extension_manager import Extension
|
||||
from lnbits.core.models import DbVersion
|
||||
from lnbits.core.models.extensions import InstallableExtension
|
||||
from lnbits.db import COCKROACH, POSTGRES, SQLITE, Connection
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .crud import update_migration_version
|
||||
|
||||
async def migrate_extension_database(
|
||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||
):
|
||||
|
||||
async def migrate_extension_database(ext: Extension, current_version):
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||
ext_db = importlib.import_module(ext.module_name).db
|
||||
except ImportError as e:
|
||||
logger.error(e)
|
||||
raise ImportError(
|
||||
f"Please make sure that the extension `{ext.code}` has a migrations file."
|
||||
)
|
||||
except ImportError as exc:
|
||||
logger.error(exc)
|
||||
raise ImportError(f"Cannot import module for extension '{ext.id}'.") from exc
|
||||
|
||||
async with ext_db.connect() as ext_conn:
|
||||
await run_migration(ext_conn, ext_migrations, ext.code, current_version)
|
||||
await run_migration(ext_conn, ext_migrations, ext.id, current_version)
|
||||
|
||||
|
||||
async def run_migration(
|
||||
db: Connection, migrations_module: Any, db_name: str, current_version: int
|
||||
db: Connection,
|
||||
migrations_module: Any,
|
||||
db_name: str,
|
||||
current_version: DbVersion | None = None,
|
||||
):
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
for key, migrate in migrations_module.__dict__.items():
|
||||
|
||||
for key, migrate in list(migrations_module.__dict__.items()):
|
||||
match = matcher.match(key)
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_version:
|
||||
if not current_version or version > current_version.version:
|
||||
logger.debug(f"running migration {db_name}.{version}")
|
||||
print(f"running migration {db_name}.{version}")
|
||||
await migrate(db)
|
||||
@@ -48,72 +58,73 @@ async def run_migration(
|
||||
await update_migration_version(conn, db_name, version)
|
||||
|
||||
|
||||
async def stop_extension_background_work(
|
||||
ext_id: str, user: str, access_token: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extensions SHOULD expose a `api_stop()` function and/or a DELETE enpoint
|
||||
at the root level of their API.
|
||||
"""
|
||||
stopped = await _stop_extension_background_work(ext_id)
|
||||
|
||||
if not stopped:
|
||||
# fallback to REST API call
|
||||
await _stop_extension_background_work_via_api(ext_id, user, access_token)
|
||||
|
||||
|
||||
async def _stop_extension_background_work(ext_id) -> bool:
|
||||
upgrade_hash = settings.extension_upgrade_hash(ext_id) or ""
|
||||
ext = Extension(ext_id, True, False, upgrade_hash=upgrade_hash)
|
||||
|
||||
try:
|
||||
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
|
||||
old_module = importlib.import_module(ext.module_name)
|
||||
|
||||
# Extensions must expose an `{ext_id}_stop()` function at the module level
|
||||
# The `api_stop()` function is for backwards compatibility (will be deprecated)
|
||||
stop_fns = [f"{ext_id}_stop", "api_stop"]
|
||||
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
|
||||
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
|
||||
|
||||
await getattr(old_module, stop_fn_name)()
|
||||
|
||||
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
|
||||
logger.warning(ex)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _stop_extension_background_work_via_api(ext_id, user, access_token):
|
||||
logger.info(
|
||||
f"Stopping background work for extension '{ext_id}' using the REST API."
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
|
||||
headers = (
|
||||
{"Authorization": "Bearer " + access_token} if access_token else None
|
||||
)
|
||||
resp = await client.delete(url=url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
logger.info(f"Stopped background work for extension '{ext_id}'.")
|
||||
except Exception as ex:
|
||||
logger.warning(
|
||||
f"Failed to stop background work for '{ext_id}' using the REST API."
|
||||
)
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
def to_valid_user_id(user_id: str) -> UUID:
|
||||
if len(user_id) < 32:
|
||||
raise ValueError("User ID must have at least 128 bits")
|
||||
try:
|
||||
int(user_id, 16)
|
||||
except Exception:
|
||||
raise ValueError("Invalid hex string for User ID.")
|
||||
except Exception as exc:
|
||||
raise ValueError("Invalid hex string for User ID.") from exc
|
||||
|
||||
return UUID(hex=user_id[:32], version=4)
|
||||
|
||||
|
||||
async def load_disabled_extension_list() -> None:
|
||||
"""Update list of extensions that have been explicitly disabled"""
|
||||
inactive_extensions = await get_installed_extensions(active=False)
|
||||
settings.lnbits_deactivated_extensions.update([e.id for e in inactive_extensions])
|
||||
|
||||
|
||||
async def migrate_databases():
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
async with core_db.connect() as conn:
|
||||
exists = False
|
||||
if conn.type == SQLITE:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
|
||||
)
|
||||
elif conn.type in {POSTGRES, COCKROACH}:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
|
||||
" AND table_name = 'dbversions'"
|
||||
)
|
||||
|
||||
if not exists:
|
||||
await core_migrations.m000_create_migrations_table(conn)
|
||||
|
||||
current_versions = await get_db_versions(conn)
|
||||
core_version = next(
|
||||
(v for v in current_versions if v.db == "core"),
|
||||
DbVersion(db="core", version=0),
|
||||
)
|
||||
await run_migration(conn, core_migrations, "core", core_version)
|
||||
|
||||
# here is the first place we can be sure that the
|
||||
# `installed_extensions` table has been created
|
||||
await load_disabled_extension_list()
|
||||
|
||||
for ext in await get_installed_extensions():
|
||||
current_version = next(
|
||||
(v for v in current_versions if v.db == ext.id),
|
||||
DbVersion(db=ext.id, version=0),
|
||||
)
|
||||
if current_version is None:
|
||||
logger.warning(
|
||||
f"Extension {ext.id} has no migration version. This should not happen."
|
||||
)
|
||||
continue
|
||||
try:
|
||||
await migrate_extension_database(ext, current_version)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error migrating extension {ext.id}: {e}")
|
||||
|
||||
logger.info("✔️ All migrations done.")
|
||||
|
||||
|
||||
def is_valid_url(url):
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import datetime
|
||||
import json
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db):
|
||||
await db.execute(
|
||||
"""
|
||||
async def m000_create_migrations_table(db: Connection):
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m001_initial(db):
|
||||
async def m001_initial(db: Connection):
|
||||
"""
|
||||
Initial LNbits tables.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT,
|
||||
pass TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
"user" TEXT NOT NULL,
|
||||
extension TEXT NOT NULL,
|
||||
@@ -40,10 +37,8 @@ async def m001_initial(db):
|
||||
|
||||
UNIQUE ("user", extension)
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
""")
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -51,10 +46,8 @@ async def m001_initial(db):
|
||||
adminkey TEXT NOT NULL,
|
||||
inkey TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
""")
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS apipayments (
|
||||
payhash TEXT NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
@@ -65,11 +58,9 @@ async def m001_initial(db):
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
UNIQUE (wallet, payhash)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
@@ -83,11 +74,10 @@ async def m001_initial(db):
|
||||
GROUP BY wallet
|
||||
)x
|
||||
GROUP BY wallet;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m002_add_fields_to_apipayments(db):
|
||||
async def m002_add_fields_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adding fields to apipayments for better accounting,
|
||||
and renaming payhash to checking_id since that is what it really is.
|
||||
@@ -100,9 +90,8 @@ async def m002_add_fields_to_apipayments(db):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
|
||||
|
||||
import json
|
||||
|
||||
rows = await (await db.execute("SELECT * FROM apipayments")).fetchall()
|
||||
result = await db.execute("SELECT * FROM apipayments")
|
||||
rows = result.mappings().all()
|
||||
for row in rows:
|
||||
if not row["memo"] or not row["memo"].startswith("#"):
|
||||
continue
|
||||
@@ -113,15 +102,15 @@ async def m002_add_fields_to_apipayments(db):
|
||||
new = row["memo"][len(prefix) :]
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?, memo = ?
|
||||
WHERE checking_id = ? AND memo = ?
|
||||
UPDATE apipayments SET extra = :extra, memo = :memo1
|
||||
WHERE checking_id = :checking_id AND memo = :memo2
|
||||
""",
|
||||
(
|
||||
json.dumps({"tag": ext}),
|
||||
new,
|
||||
row["checking_id"],
|
||||
row["memo"],
|
||||
),
|
||||
{
|
||||
"extra": json.dumps({"tag": ext}),
|
||||
"memo1": new,
|
||||
"checking_id": row["checking_id"],
|
||||
"memo2": row["memo"],
|
||||
},
|
||||
)
|
||||
break
|
||||
except OperationalError:
|
||||
@@ -132,7 +121,7 @@ async def m002_add_fields_to_apipayments(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m003_add_invoice_webhook(db):
|
||||
async def m003_add_invoice_webhook(db: Connection):
|
||||
"""
|
||||
Special column for webhook endpoints that can be assigned
|
||||
to each different invoice.
|
||||
@@ -142,14 +131,13 @@ async def m003_add_invoice_webhook(db):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
|
||||
|
||||
|
||||
async def m004_ensure_fees_are_always_negative(db):
|
||||
async def m004_ensure_fees_are_always_negative(db: Connection):
|
||||
"""
|
||||
Use abs() so wallet backends don't have to care about the sign of the fees.
|
||||
"""
|
||||
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT wallet, SUM(amount) AS s -- incoming
|
||||
@@ -163,18 +151,16 @@ async def m004_ensure_fees_are_always_negative(db):
|
||||
GROUP BY wallet
|
||||
)x
|
||||
GROUP BY wallet;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m005_balance_check_balance_notify(db):
|
||||
async def m005_balance_check_balance_notify(db: Connection):
|
||||
"""
|
||||
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
|
||||
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_check (
|
||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||
service TEXT NOT NULL,
|
||||
@@ -182,22 +168,19 @@ async def m005_balance_check_balance_notify(db):
|
||||
|
||||
UNIQUE(wallet, service)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_notify (
|
||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||
url TEXT NOT NULL,
|
||||
|
||||
UNIQUE(wallet, url)
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m006_add_invoice_expiry_to_apipayments(db):
|
||||
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adds invoice expiry column to apipayments.
|
||||
"""
|
||||
@@ -207,24 +190,24 @@ async def m006_add_invoice_expiry_to_apipayments(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m007_set_invoice_expiries(db):
|
||||
async def m007_set_invoice_expiries(db: Connection):
|
||||
"""
|
||||
Precomputes invoice expiry for existing pending incoming payments.
|
||||
"""
|
||||
try:
|
||||
rows = await (
|
||||
await db.execute(
|
||||
f"""
|
||||
SELECT bolt11, checking_id
|
||||
FROM apipayments
|
||||
WHERE pending = true
|
||||
AND amount > 0
|
||||
AND bolt11 IS NOT NULL
|
||||
AND expiry IS NULL
|
||||
AND time < {db.timestamp_now}
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
result = await db.execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
SELECT bolt11, checking_id
|
||||
FROM apipayments
|
||||
WHERE pending = true
|
||||
AND amount > 0
|
||||
AND bolt11 IS NOT NULL
|
||||
AND expiry IS NULL
|
||||
AND time < {db.timestamp_now}
|
||||
""" # noqa: S608
|
||||
)
|
||||
rows = result.mappings().all()
|
||||
if len(rows):
|
||||
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
|
||||
for i, (
|
||||
@@ -236,24 +219,21 @@ async def m007_set_invoice_expiries(db):
|
||||
if invoice.expiry is None:
|
||||
continue
|
||||
|
||||
expiration_date = datetime.datetime.fromtimestamp(
|
||||
invoice.date + invoice.expiry
|
||||
)
|
||||
expiration_date = invoice.date + invoice.expiry
|
||||
logger.info(
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
|
||||
f" {invoice.payment_hash} to {expiration_date}"
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET expiry = ?
|
||||
WHERE checking_id = ? AND amount > 0
|
||||
""",
|
||||
(
|
||||
db.datetime_to_timestamp(expiration_date),
|
||||
checking_id,
|
||||
),
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE apipayments SET expiry = {db.timestamp_placeholder('expiry')}
|
||||
WHERE checking_id = :checking_id AND amount > 0
|
||||
""", # noqa: S608
|
||||
{"expiry": expiration_date, "checking_id": checking_id},
|
||||
)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
continue
|
||||
except OperationalError:
|
||||
# this is necessary now because it may be the case that this migration will
|
||||
@@ -263,20 +243,17 @@ async def m007_set_invoice_expiries(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m008_create_admin_settings_table(db):
|
||||
await db.execute(
|
||||
"""
|
||||
async def m008_create_admin_settings_table(db: Connection):
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
super_user TEXT,
|
||||
editable_settings TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m009_create_tinyurl_table(db):
|
||||
await db.execute(
|
||||
f"""
|
||||
async def m009_create_tinyurl_table(db: Connection):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT,
|
||||
@@ -284,13 +261,11 @@ async def m009_create_tinyurl_table(db):
|
||||
wallet TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m010_create_installed_extensions_table(db):
|
||||
await db.execute(
|
||||
"""
|
||||
async def m010_create_installed_extensions_table(db: Connection):
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||
id TEXT PRIMARY KEY,
|
||||
version TEXT NOT NULL,
|
||||
@@ -301,36 +276,31 @@ async def m010_create_installed_extensions_table(db):
|
||||
active BOOLEAN DEFAULT false,
|
||||
meta TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m011_optimize_balances_view(db):
|
||||
async def m011_optimize_balances_view(db: Connection):
|
||||
"""
|
||||
Make the calculation of the balance a single aggregation
|
||||
over the payments table instead of 2.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute(
|
||||
"""
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
||||
FROM apipayments
|
||||
WHERE (pending = false AND amount > 0) OR amount < 0
|
||||
GROUP BY wallet
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m012_add_currency_to_wallet(db):
|
||||
await db.execute(
|
||||
"""
|
||||
async def m012_add_currency_to_wallet(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m013_add_deleted_to_wallets(db):
|
||||
async def m013_add_deleted_to_wallets(db: Connection):
|
||||
"""
|
||||
Adds deleted column to wallets.
|
||||
"""
|
||||
@@ -342,36 +312,40 @@ async def m013_add_deleted_to_wallets(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m014_set_deleted_wallets(db):
|
||||
async def m014_set_deleted_wallets(db: Connection):
|
||||
"""
|
||||
Sets deleted column to wallets.
|
||||
"""
|
||||
try:
|
||||
rows = await (
|
||||
await db.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM wallets
|
||||
WHERE user LIKE 'del:%'
|
||||
AND adminkey LIKE 'del:%'
|
||||
AND inkey LIKE 'del:%'
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
result = await db.execute("""
|
||||
SELECT *
|
||||
FROM wallets
|
||||
WHERE user LIKE 'del:%'
|
||||
AND adminkey LIKE 'del:%'
|
||||
AND inkey LIKE 'del:%'
|
||||
""")
|
||||
rows = result.mappings().all()
|
||||
|
||||
for row in rows:
|
||||
try:
|
||||
user = row[2].split(":")[1]
|
||||
adminkey = row[3].split(":")[1]
|
||||
inkey = row[4].split(":")[1]
|
||||
user = row["user"].split(":")[1]
|
||||
adminkey = row["adminkey"].split(":")[1]
|
||||
inkey = row["inkey"].split(":")[1]
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE wallets SET user = ?, adminkey = ?, inkey = ?, deleted = true
|
||||
WHERE id = ?
|
||||
UPDATE wallets SET
|
||||
"user" = :user, adminkey = :adminkey, inkey = :inkey, deleted = true
|
||||
WHERE id = :wallet
|
||||
""",
|
||||
(user, adminkey, inkey, row[0]),
|
||||
{
|
||||
"user": user,
|
||||
"adminkey": adminkey,
|
||||
"inkey": inkey,
|
||||
"wallet": row.get("id"),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.debug(exc)
|
||||
continue
|
||||
except OperationalError:
|
||||
# this is necessary now because it may be the case that this migration will
|
||||
@@ -381,9 +355,8 @@ async def m014_set_deleted_wallets(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m015_create_push_notification_subscriptions_table(db):
|
||||
await db.execute(
|
||||
f"""
|
||||
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||
endpoint TEXT NOT NULL,
|
||||
"user" TEXT NOT NULL,
|
||||
@@ -392,11 +365,10 @@ async def m015_create_push_notification_subscriptions_table(db):
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
PRIMARY KEY (endpoint, "user")
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def m016_add_username_column_to_accounts(db):
|
||||
async def m016_add_username_column_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds username column to accounts.
|
||||
"""
|
||||
@@ -407,7 +379,7 @@ async def m016_add_username_column_to_accounts(db):
|
||||
pass
|
||||
|
||||
|
||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
||||
async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
|
||||
"""
|
||||
Adds created_at and updated_at column to accounts and wallets.
|
||||
"""
|
||||
@@ -454,20 +426,379 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
|
||||
# set all to now where they are null
|
||||
now = int(time())
|
||||
await db.execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE wallets SET created_at = {db.timestamp_placeholder}
|
||||
UPDATE wallets SET created_at = {db.timestamp_placeholder('now')}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
(now,),
|
||||
""", # noqa: S608
|
||||
{"now": now},
|
||||
)
|
||||
await db.execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE accounts SET created_at = {db.timestamp_placeholder}
|
||||
UPDATE accounts SET created_at = {db.timestamp_placeholder('now')}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
(now,),
|
||||
""", # noqa: S608
|
||||
{"now": now},
|
||||
)
|
||||
|
||||
except OperationalError as exc:
|
||||
logger.error(f"Migration 17 failed: {exc}")
|
||||
pass
|
||||
|
||||
|
||||
async def m018_balances_view_exclude_deleted(db: Connection):
|
||||
"""
|
||||
Make deleted wallets not show up in the balances view.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
FROM apipayments
|
||||
LEFT JOIN wallets ON apipayments.wallet = wallets.id
|
||||
WHERE (wallets.deleted = false OR wallets.deleted is NULL)
|
||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||
OR apipayments.amount < 0)
|
||||
GROUP BY wallet
|
||||
""")
|
||||
|
||||
|
||||
async def m019_balances_view_based_on_wallets(db: Connection):
|
||||
"""
|
||||
Make deleted wallets not show up in the balances view.
|
||||
Important for querying whole lnbits balances.
|
||||
"""
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
FROM wallets
|
||||
LEFT JOIN apipayments ON apipayments.wallet = wallets.id
|
||||
WHERE (wallets.deleted = false OR wallets.deleted is NULL)
|
||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||
OR apipayments.amount < 0)
|
||||
GROUP BY apipayments.wallet
|
||||
""")
|
||||
|
||||
|
||||
async def m020_add_column_column_to_user_extensions(db: Connection):
|
||||
"""
|
||||
Adds extra column to user extensions.
|
||||
"""
|
||||
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")
|
||||
|
||||
|
||||
async def m021_add_success_failed_to_apipayments(db: Connection):
|
||||
"""
|
||||
Adds success and failed columns to apipayments.
|
||||
"""
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN status TEXT DEFAULT 'pending'")
|
||||
# set all not pending to success true, failed payments were deleted until now
|
||||
await db.execute("UPDATE apipayments SET status = 'success' WHERE NOT pending")
|
||||
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
FROM wallets
|
||||
LEFT JOIN apipayments ON apipayments.wallet = wallets.id
|
||||
WHERE (wallets.deleted = false OR wallets.deleted is NULL)
|
||||
AND (
|
||||
(apipayments.status = 'success' AND apipayments.amount > 0)
|
||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||
)
|
||||
GROUP BY apipayments.wallet
|
||||
""")
|
||||
|
||||
|
||||
async def m022_add_pubkey_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds pubkey column to accounts.
|
||||
"""
|
||||
try:
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN pubkey TEXT")
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
|
||||
async def m023_add_column_column_to_apipayments(db: Connection):
|
||||
"""
|
||||
renames hash to payment_hash and drops unused index
|
||||
"""
|
||||
await db.execute("DROP INDEX by_hash")
|
||||
await db.execute("ALTER TABLE apipayments RENAME COLUMN hash TO payment_hash")
|
||||
await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id")
|
||||
await db.execute("ALTER TABLE accounts RENAME COLUMN pass TO password_hash")
|
||||
|
||||
await db.execute("CREATE INDEX by_hash ON apipayments (payment_hash)")
|
||||
|
||||
|
||||
async def m024_drop_pending(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
|
||||
|
||||
|
||||
async def m025_refresh_view(db: Connection):
|
||||
await db.execute("DROP VIEW balances")
|
||||
await db.execute("""
|
||||
CREATE VIEW balances AS
|
||||
SELECT apipayments.wallet_id,
|
||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||
FROM wallets
|
||||
LEFT JOIN apipayments ON apipayments.wallet_id = wallets.id
|
||||
WHERE (wallets.deleted = false OR wallets.deleted is NULL)
|
||||
AND (
|
||||
(apipayments.status = 'success' AND apipayments.amount > 0)
|
||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||
)
|
||||
GROUP BY apipayments.wallet_id
|
||||
""")
|
||||
|
||||
|
||||
async def m026_update_payment_table(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN tag TEXT")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN extension TEXT")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN created_at TIMESTAMP")
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN updated_at TIMESTAMP")
|
||||
|
||||
|
||||
async def m027_update_apipayments_data(db: Connection):
|
||||
result = None
|
||||
try:
|
||||
result = await db.execute("SELECT * FROM apipayments LIMIT 100")
|
||||
except Exception as exc:
|
||||
logger.warning("Could not select, trying again after cache cleared.")
|
||||
logger.debug(exc)
|
||||
await db.execute("COMMIT")
|
||||
|
||||
offset = 0
|
||||
limit = 1000
|
||||
payments: list[dict[Any, Any]] = []
|
||||
logger.info("Updating payments")
|
||||
while len(payments) > 0 or offset == 0:
|
||||
logger.info(f"Updating {offset} to {offset+limit}")
|
||||
|
||||
result = await db.execute(
|
||||
# Limit and Offset safe from SQL injection
|
||||
# since they are integers and are not user input
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
ORDER BY time LIMIT {int(limit)} OFFSET {int(offset)}
|
||||
""" # noqa: S608
|
||||
)
|
||||
payments = result.mappings().all()
|
||||
logger.info(f"Payments count: {len(payments)}")
|
||||
|
||||
for payment in payments:
|
||||
tag = None
|
||||
created_at = payment.get("time")
|
||||
if payment.get("extra"):
|
||||
extra = json.loads(str(payment.get("extra")))
|
||||
tag = extra.get("tag")
|
||||
tsph = db.timestamp_placeholder("created_at")
|
||||
await db.execute(
|
||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||
f"""
|
||||
UPDATE apipayments
|
||||
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
|
||||
WHERE checking_id = :checking_id
|
||||
""", # noqa: S608
|
||||
{
|
||||
"tag": tag,
|
||||
"created_at": created_at,
|
||||
"checking_id": payment.get("checking_id"),
|
||||
},
|
||||
)
|
||||
offset += limit
|
||||
logger.info("Payments updated")
|
||||
|
||||
|
||||
async def m028_update_settings(db: Connection):
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
id TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
tag TEXT NOT NULL DEFAULT 'core',
|
||||
|
||||
UNIQUE (id, tag)
|
||||
);
|
||||
""")
|
||||
|
||||
async def _insert_key_value(id_: str, value: Any):
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO system_settings (id, value, tag)
|
||||
VALUES (:id, :value, :tag)
|
||||
""",
|
||||
{"id": id_, "value": json.dumps(value), "tag": "core"},
|
||||
)
|
||||
|
||||
row: dict = await db.fetchone("SELECT * FROM settings")
|
||||
if row:
|
||||
await _insert_key_value("super_user", row["super_user"])
|
||||
editable_settings = json.loads(row["editable_settings"])
|
||||
|
||||
for key, value in editable_settings.items():
|
||||
await _insert_key_value(key, value)
|
||||
|
||||
await db.execute("drop table settings")
|
||||
|
||||
|
||||
async def m029_create_audit_table(db: Connection):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS audit (
|
||||
component TEXT,
|
||||
ip_address TEXT,
|
||||
user_id TEXT,
|
||||
path TEXT,
|
||||
request_type TEXT,
|
||||
request_method TEXT,
|
||||
request_details TEXT,
|
||||
response_code TEXT,
|
||||
duration REAL NOT NULL,
|
||||
delete_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m030_add_user_api_tokens_column(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
||||
""")
|
||||
|
||||
|
||||
async def m031_add_color_and_icon_to_wallets(db: Connection):
|
||||
"""
|
||||
Adds icon and color columns to wallets.
|
||||
"""
|
||||
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
|
||||
|
||||
|
||||
async def m032_add_external_id_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds external_id column to accounts.
|
||||
Used for external account linking.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN external_id TEXT")
|
||||
|
||||
|
||||
async def m033_update_payment_table(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||
|
||||
|
||||
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||
""")
|
||||
|
||||
|
||||
async def m035_add_wallet_type_column(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||
""")
|
||||
|
||||
|
||||
async def m036_add_shared_wallet_column(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||
""")
|
||||
|
||||
|
||||
async def m037_create_assets_table(db: Connection):
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
is_public BOOLEAN NOT NULL DEFAULT false,
|
||||
name TEXT NOT NULL,
|
||||
size_bytes INT NOT NULL,
|
||||
thumbnail_base64 TEXT,
|
||||
thumbnail {db.blob},
|
||||
data {db.blob} NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
async def m038_add_labels_for_payments(db: Connection):
|
||||
await db.execute("""
|
||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||
""")
|
||||
|
||||
|
||||
async def m039_index_payments(db: Connection):
|
||||
indexes = [
|
||||
"wallet_id",
|
||||
"checking_id",
|
||||
"payment_hash",
|
||||
"amount",
|
||||
"fee",
|
||||
"labels",
|
||||
"time",
|
||||
"status",
|
||||
"memo",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_payments_{index}...")
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
||||
""")
|
||||
|
||||
|
||||
async def m040_index_wallets(db: Connection):
|
||||
indexes = [
|
||||
"id",
|
||||
"user",
|
||||
"deleted",
|
||||
"adminkey",
|
||||
"inkey",
|
||||
"wallet_type",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
||||
""")
|
||||
|
||||
|
||||
async def m042_index_accounts(db: Connection):
|
||||
indexes = [
|
||||
"id",
|
||||
"email",
|
||||
"username",
|
||||
"pubkey",
|
||||
"external_id",
|
||||
]
|
||||
|
||||
for index in indexes:
|
||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||
await db.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||
""")
|
||||
|
||||
|
||||
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds ui_customization column to accounts.
|
||||
Used for server side persistence of UI customization settings.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||
|
||||
|
||||
async def m044_add_activated_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds activated column to accounts.
|
||||
Used for account activation status.
|
||||
"""
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
from fastapi import Query
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection, FilterModel, FromRowModel
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus
|
||||
|
||||
|
||||
class BaseWallet(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class Wallet(BaseWallet):
|
||||
user: str
|
||||
currency: Optional[str]
|
||||
deleted: bool
|
||||
created_at: Optional[int] = None
|
||||
updated_at: Optional[int] = None
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
return self.balance_msat // 1000
|
||||
|
||||
@property
|
||||
def withdrawable_balance(self) -> int:
|
||||
from .services import fee_reserve
|
||||
|
||||
return self.balance_msat - fee_reserve(self.balance_msat)
|
||||
|
||||
@property
|
||||
def lnurlwithdraw_full(self) -> str:
|
||||
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
||||
try:
|
||||
return lnurl_encode(url)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
||||
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
||||
|
||||
return SigningKey.from_string(
|
||||
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
||||
)
|
||||
|
||||
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
||||
from .crud import get_standalone_payment
|
||||
|
||||
return await get_standalone_payment(payment_hash)
|
||||
|
||||
|
||||
class WalletType(Enum):
|
||||
admin = 0
|
||||
invoice = 1
|
||||
invalid = 2
|
||||
|
||||
# backwards compatibility
|
||||
def __eq__(self, other):
|
||||
return self.value == other
|
||||
|
||||
|
||||
@dataclass
|
||||
class WalletTypeInfo:
|
||||
wallet_type: WalletType
|
||||
wallet: Wallet
|
||||
|
||||
|
||||
class UserConfig(BaseModel):
|
||||
email_verified: Optional[bool] = False
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
# Auth provider, possible values:
|
||||
# - "env": the user was created automatically by the system
|
||||
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
||||
# - "google | github | ...": the user was created using an SSO provider
|
||||
provider: Optional[str] = "lnbits" # auth provider
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
email: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
extensions: List[str] = []
|
||||
wallets: List[Wallet] = []
|
||||
admin: bool = False
|
||||
super_user: bool = False
|
||||
has_password: bool = False
|
||||
config: Optional[UserConfig] = None
|
||||
created_at: Optional[int] = None
|
||||
updated_at: Optional[int] = None
|
||||
|
||||
@property
|
||||
def wallet_ids(self) -> List[str]:
|
||||
return [wallet.id for wallet in self.wallets]
|
||||
|
||||
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||
return w[0] if w else None
|
||||
|
||||
@classmethod
|
||||
def is_extension_for_user(cls, ext: str, user: str) -> bool:
|
||||
if ext not in settings.lnbits_admin_extensions:
|
||||
return True
|
||||
if user == settings.super_user:
|
||||
return True
|
||||
if user in settings.lnbits_admin_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
email: Optional[str] = Query(default=None)
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
|
||||
|
||||
class UpdateUser(BaseModel):
|
||||
user_id: str
|
||||
email: Optional[str] = Query(default=None)
|
||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||
config: Optional[UserConfig] = None
|
||||
|
||||
|
||||
class UpdateUserPassword(BaseModel):
|
||||
user_id: str
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_old: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
||||
|
||||
|
||||
class UpdateSuperuserPassword(BaseModel):
|
||||
username: str = Query(default=..., min_length=2, max_length=20)
|
||||
password: str = Query(default=..., min_length=8, max_length=50)
|
||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||
|
||||
|
||||
class LoginUsr(BaseModel):
|
||||
usr: str
|
||||
|
||||
|
||||
class LoginUsernamePassword(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class Payment(FromRowModel):
|
||||
checking_id: str
|
||||
pending: bool
|
||||
amount: int
|
||||
fee: int
|
||||
memo: Optional[str]
|
||||
time: int
|
||||
bolt11: str
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[float]
|
||||
extra: Dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
checking_id=row["checking_id"],
|
||||
payment_hash=row["hash"] or "0" * 64,
|
||||
bolt11=row["bolt11"] or "",
|
||||
preimage=row["preimage"] or "0" * 64,
|
||||
extra=json.loads(row["extra"] or "{}"),
|
||||
pending=row["pending"],
|
||||
amount=row["amount"],
|
||||
fee=row["fee"],
|
||||
memo=row["memo"],
|
||||
time=row["time"],
|
||||
expiry=row["expiry"],
|
||||
wallet_id=row["wallet"],
|
||||
webhook=row["webhook"],
|
||||
webhook_status=row["webhook_status"],
|
||||
)
|
||||
|
||||
@property
|
||||
def tag(self) -> Optional[str]:
|
||||
if self.extra is None:
|
||||
return ""
|
||||
return self.extra.get("tag")
|
||||
|
||||
@property
|
||||
def msat(self) -> int:
|
||||
return self.amount
|
||||
|
||||
@property
|
||||
def sat(self) -> int:
|
||||
return self.amount // 1000
|
||||
|
||||
@property
|
||||
def is_in(self) -> bool:
|
||||
return self.amount > 0
|
||||
|
||||
@property
|
||||
def is_out(self) -> bool:
|
||||
return self.amount < 0
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return self.expiry < time.time() if self.expiry else False
|
||||
|
||||
@property
|
||||
def is_uncheckable(self) -> bool:
|
||||
return self.checking_id.startswith("internal_")
|
||||
|
||||
async def update_status(
|
||||
self,
|
||||
status: PaymentStatus,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
from .crud import update_payment_details
|
||||
|
||||
await update_payment_details(
|
||||
checking_id=self.checking_id,
|
||||
pending=status.pending,
|
||||
fee=status.fee_msat,
|
||||
preimage=status.preimage,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
async def set_pending(self, pending: bool) -> None:
|
||||
from .crud import update_payment_status
|
||||
|
||||
self.pending = pending
|
||||
|
||||
await update_payment_status(self.checking_id, pending)
|
||||
|
||||
async def check_status(
|
||||
self,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> PaymentStatus:
|
||||
if self.is_uncheckable:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
logger.debug(
|
||||
f"Checking {'outgoing' if self.is_out else 'incoming'} "
|
||||
f"pending payment {self.checking_id}"
|
||||
)
|
||||
|
||||
WALLET = get_wallet_class()
|
||||
if self.is_out:
|
||||
status = await WALLET.get_payment_status(self.checking_id)
|
||||
else:
|
||||
status = await WALLET.get_invoice_status(self.checking_id)
|
||||
|
||||
logger.debug(f"Status: {status}")
|
||||
|
||||
if self.is_in and status.pending and self.is_expired and self.expiry:
|
||||
expiration_date = datetime.datetime.fromtimestamp(self.expiry)
|
||||
logger.debug(
|
||||
f"Deleting expired incoming pending payment {self.checking_id}: "
|
||||
f"expired {expiration_date}"
|
||||
)
|
||||
await self.delete(conn)
|
||||
# wait at least 15 minutes before deleting failed outgoing payments
|
||||
elif self.is_out and status.failed:
|
||||
if self.time + 900 < int(time.time()):
|
||||
logger.warning(
|
||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||
)
|
||||
await self.delete(conn)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Tried to delete outgoing payment {self.checking_id}: "
|
||||
"skipping because it's not old enough"
|
||||
)
|
||||
elif not status.pending:
|
||||
logger.info(
|
||||
f"Marking '{'in' if self.is_in else 'out'}' "
|
||||
f"{self.checking_id} as not pending anymore: {status}"
|
||||
)
|
||||
await self.update_status(status, conn=conn)
|
||||
return status
|
||||
|
||||
async def delete(self, conn: Optional[Connection] = None) -> None:
|
||||
from .crud import delete_wallet_payment
|
||||
|
||||
await delete_wallet_payment(self.checking_id, self.wallet_id, conn=conn)
|
||||
|
||||
|
||||
class PaymentFilters(FilterModel):
|
||||
__search_fields__ = ["memo", "amount"]
|
||||
|
||||
checking_id: str
|
||||
amount: int
|
||||
fee: int
|
||||
memo: Optional[str]
|
||||
time: datetime.datetime
|
||||
bolt11: str
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[datetime.datetime]
|
||||
extra: Dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
|
||||
|
||||
class PaymentHistoryPoint(BaseModel):
|
||||
date: datetime.datetime
|
||||
income: int
|
||||
spending: int
|
||||
balance: int
|
||||
|
||||
|
||||
class BalanceCheck(BaseModel):
|
||||
wallet: str
|
||||
service: str
|
||||
url: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
|
||||
|
||||
|
||||
def _do_nothing(*_):
|
||||
pass
|
||||
|
||||
|
||||
class CoreAppExtra:
|
||||
register_new_ext_routes: Callable = _do_nothing
|
||||
register_new_ratelimiter: Callable
|
||||
|
||||
|
||||
class TinyURL(BaseModel):
|
||||
id: str
|
||||
url: str
|
||||
endless: bool
|
||||
wallet: str
|
||||
time: float
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class ConversionData(BaseModel):
|
||||
from_: str = "sat"
|
||||
amount: float
|
||||
to: str = "usd"
|
||||
|
||||
|
||||
class Callback(BaseModel):
|
||||
callback: str
|
||||
|
||||
|
||||
class DecodePayment(BaseModel):
|
||||
data: str
|
||||
|
||||
|
||||
class CreateLnurl(BaseModel):
|
||||
description_hash: str
|
||||
callback: str
|
||||
amount: int
|
||||
comment: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
|
||||
|
||||
class CreateInvoice(BaseModel):
|
||||
unit: str = "sat"
|
||||
internal: bool = False
|
||||
out: bool = True
|
||||
amount: float = Query(None, ge=0)
|
||||
memo: Optional[str] = None
|
||||
description_hash: Optional[str] = None
|
||||
unhashed_description: Optional[str] = None
|
||||
expiry: Optional[int] = None
|
||||
lnurl_callback: Optional[str] = None
|
||||
lnurl_balance_check: Optional[str] = None
|
||||
extra: Optional[dict] = None
|
||||
webhook: Optional[str] = None
|
||||
bolt11: Optional[str] = None
|
||||
|
||||
|
||||
class CreateTopup(BaseModel):
|
||||
id: str
|
||||
amount: int
|
||||
|
||||
|
||||
class CreateLnurlAuth(BaseModel):
|
||||
callback: str
|
||||
|
||||
|
||||
class CreateWallet(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class CreateWebPushSubscription(BaseModel):
|
||||
subscription: str
|
||||
|
||||
|
||||
class WebPushSubscription(BaseModel):
|
||||
endpoint: str
|
||||
user: str
|
||||
data: str
|
||||
host: str
|
||||
timestamp: str
|
||||
@@ -0,0 +1,104 @@
|
||||
from .audit import AuditEntry, AuditFilters
|
||||
from .lnurl import CreateLnurlPayment, CreateLnurlWithdraw
|
||||
from .misc import (
|
||||
BalanceDelta,
|
||||
Callback,
|
||||
ConversionData,
|
||||
CoreAppExtra,
|
||||
DbVersion,
|
||||
SimpleStatus,
|
||||
)
|
||||
from .payments import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
CreatePayment,
|
||||
DecodePayment,
|
||||
PayInvoice,
|
||||
Payment,
|
||||
PaymentCountField,
|
||||
PaymentCountStat,
|
||||
PaymentDailyStats,
|
||||
PaymentExtra,
|
||||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
PaymentsStatusCount,
|
||||
PaymentState,
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
)
|
||||
from .tinyurl import TinyURL
|
||||
from .users import (
|
||||
AccessTokenPayload,
|
||||
Account,
|
||||
AccountFilters,
|
||||
AccountOverview,
|
||||
CreateUser,
|
||||
LoginUsernamePassword,
|
||||
LoginUsr,
|
||||
RegisterUser,
|
||||
ResetUserPassword,
|
||||
UpdateBalance,
|
||||
UpdateSuperuserPassword,
|
||||
UpdateUser,
|
||||
UpdateUserPassword,
|
||||
UpdateUserPubkey,
|
||||
User,
|
||||
UserAcls,
|
||||
UserExtra,
|
||||
)
|
||||
from .wallets import CreateWallet, KeyType, Wallet, WalletInfo, WalletTypeInfo
|
||||
from .webpush import CreateWebPushSubscription, WebPushSubscription
|
||||
|
||||
__all__ = [
|
||||
"AccessTokenPayload",
|
||||
"Account",
|
||||
"AccountFilters",
|
||||
"AccountOverview",
|
||||
"AuditEntry",
|
||||
"AuditFilters",
|
||||
"BalanceDelta",
|
||||
"Callback",
|
||||
"CancelInvoice",
|
||||
"ConversionData",
|
||||
"CoreAppExtra",
|
||||
"CreateInvoice",
|
||||
"CreateLnurlPayment",
|
||||
"CreateLnurlWithdraw",
|
||||
"CreatePayment",
|
||||
"CreateUser",
|
||||
"CreateWallet",
|
||||
"CreateWebPushSubscription",
|
||||
"DbVersion",
|
||||
"DecodePayment",
|
||||
"KeyType",
|
||||
"LoginUsernamePassword",
|
||||
"LoginUsr",
|
||||
"PayInvoice",
|
||||
"Payment",
|
||||
"PaymentCountField",
|
||||
"PaymentCountStat",
|
||||
"PaymentDailyStats",
|
||||
"PaymentExtra",
|
||||
"PaymentFilters",
|
||||
"PaymentHistoryPoint",
|
||||
"PaymentState",
|
||||
"PaymentWalletStats",
|
||||
"PaymentsStatusCount",
|
||||
"RegisterUser",
|
||||
"ResetUserPassword",
|
||||
"SettleInvoice",
|
||||
"SimpleStatus",
|
||||
"TinyURL",
|
||||
"UpdateBalance",
|
||||
"UpdateSuperuserPassword",
|
||||
"UpdateUser",
|
||||
"UpdateUserPassword",
|
||||
"UpdateUserPubkey",
|
||||
"User",
|
||||
"UserAcls",
|
||||
"UserExtra",
|
||||
"Wallet",
|
||||
"WalletInfo",
|
||||
"WalletTypeInfo",
|
||||
"WebPushSubscription",
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from lnbits.db import FilterModel
|
||||
|
||||
|
||||
class AssetInfo(BaseModel):
|
||||
id: str
|
||||
mime_type: str
|
||||
name: str
|
||||
is_public: bool = False
|
||||
size_bytes: int
|
||||
thumbnail_base64: str | None = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class Asset(AssetInfo):
|
||||
user_id: str
|
||||
data: bytes
|
||||
|
||||
|
||||
class AssetUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
is_public: bool | None = None
|
||||
|
||||
|
||||
class AssetFilters(FilterModel):
|
||||
__search_fields__ = ["name"]
|
||||
__sort_fields__ = [
|
||||
"created_at",
|
||||
"name",
|
||||
]
|
||||
|
||||
name: str | None = None
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from lnbits.db import FilterModel
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
class AuditEntry(BaseModel):
|
||||
component: str | None = None
|
||||
ip_address: str | None = None
|
||||
user_id: str | None = None
|
||||
path: str | None = None
|
||||
request_type: str | None = None
|
||||
request_method: str | None = None
|
||||
request_details: str | None = None
|
||||
response_code: str | None = None
|
||||
duration: float
|
||||
delete_at: datetime | None = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
retention_days = max(0, settings.lnbits_audit_retention_days) or 365
|
||||
self.delete_at = self.created_at + timedelta(days=retention_days)
|
||||
|
||||
|
||||
class AuditFilters(FilterModel):
|
||||
__search_fields__ = [
|
||||
"ip_address",
|
||||
"user_id",
|
||||
"path",
|
||||
"request_method",
|
||||
"response_code",
|
||||
"component",
|
||||
]
|
||||
__sort_fields__ = [
|
||||
"created_at",
|
||||
"duration",
|
||||
]
|
||||
|
||||
ip_address: str | None = None
|
||||
user_id: str | None = None
|
||||
path: str | None = None
|
||||
request_method: str | None = None
|
||||
response_code: str | None = None
|
||||
component: str | None = None
|
||||
|
||||
|
||||
class AuditCountStat(BaseModel):
|
||||
field: str = ""
|
||||
total: float = 0
|
||||
|
||||
|
||||
class AuditStats(BaseModel):
|
||||
request_method: list[AuditCountStat] = []
|
||||
response_code: list[AuditCountStat] = []
|
||||
component: list[AuditCountStat] = []
|
||||
long_duration: list[AuditCountStat] = []
|
||||