Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de2a62159 | ||
|
|
dda1d9bb3a | ||
|
|
c0c4c86808 | ||
|
|
7322d27aa3 | ||
|
|
e8f78307f3 | ||
|
|
6c9a52c6e1 | ||
|
|
caadeb77e2 | ||
|
|
5392b2cbfe | ||
|
|
274615f0b3 | ||
|
|
e53ccb04ac | ||
|
|
0d0817044c | ||
|
|
3c61f61588 | ||
|
|
87ef8638b5 | ||
|
|
39559cccb9 | ||
|
|
f3b1bca5b8 | ||
|
|
165e5099c1 | ||
|
|
617b1c941b | ||
|
|
331c8e593b | ||
|
|
2976168387 | ||
|
|
e106ec3e9c | ||
|
|
a089188664 | ||
|
|
9693e5932b | ||
|
|
d30a6dc5c1 | ||
|
|
ca51b57355 | ||
|
|
b13588dcef | ||
|
|
ffb5a91f67 | ||
|
|
dcf5f0adaa | ||
|
|
d89a6a337a | ||
|
|
d8b5e3872b | ||
|
|
0e1f9dbb5a | ||
|
|
2a2a01d3df | ||
|
|
a8b32194eb | ||
|
|
99d963c726 | ||
|
|
5b6e05af60 | ||
|
|
bfc99937f6 | ||
|
|
25fce6ba7c | ||
|
|
71fc1b7146 | ||
|
|
068fc67ade | ||
|
|
266471a5e3 | ||
|
|
df68400a25 | ||
|
|
01c2b4fe56 | ||
|
|
ca4c4ac66b | ||
|
|
a7e8593a9a | ||
|
|
f11b6289b1 | ||
|
|
b3aa471125 | ||
|
|
985bfeaae7 | ||
|
|
95f762b687 | ||
|
|
813c0aa48c | ||
|
|
f3439edfaa | ||
|
|
1e7995927d | ||
|
|
8f63ca5f56 | ||
|
|
025b1b251d | ||
|
|
9d3290cd9a | ||
|
|
23cca85739 | ||
|
|
6e16094572 | ||
|
|
003ad7218c | ||
|
|
9a0a9aa779 | ||
|
|
251c457610 | ||
|
|
9988f5167e | ||
|
|
cb7e311c8d | ||
|
|
079d335648 | ||
|
|
2b3b8cd85e | ||
|
|
615734e4a9 | ||
|
|
17f5b734b2 | ||
|
|
2eb76d0e26 | ||
|
|
265ea16d30 | ||
|
|
20e6ca019e | ||
|
|
ca4134eb02 | ||
|
|
f97326bed0 | ||
|
|
6e33784061 | ||
|
|
6d8896c2f6 | ||
|
|
e28a6c228e | ||
|
|
aafd955bb4 | ||
|
|
ce701b9506 | ||
|
|
2b3a2afe83 | ||
|
|
f624b54cfb | ||
|
|
492eda5fdc | ||
|
|
97fd5eac8f | ||
|
|
717ae540b0 | ||
|
|
3e9bb25b76 | ||
|
|
ad3b422e9f | ||
|
|
c0b1f7f8d4 | ||
|
|
6084ac1633 | ||
|
|
d96aabc14c | ||
|
|
ca4df159e1 | ||
|
|
bb57831c53 | ||
|
|
08c485e34b | ||
|
|
a3f7636db0 | ||
|
|
1cc3801e38 | ||
|
|
12688efd5f | ||
|
|
76e84359ce | ||
|
|
731ebf9f85 | ||
|
|
936e9342a2 | ||
|
|
a5825f1aa9 | ||
|
|
85afb39388 | ||
|
|
569990a760 | ||
|
|
a813a37e68 | ||
|
|
5f60b025d5 | ||
|
|
a0c66871fa | ||
|
|
c3ded0b51c | ||
|
|
701b66e71e | ||
|
|
affcb9feca | ||
|
|
8efa81136b | ||
|
|
5b51ad6437 | ||
|
|
fc7a799b89 | ||
|
|
6d25fa4fa0 | ||
|
|
4397516160 | ||
|
|
e886d17f15 | ||
|
|
1f23d45d11 | ||
|
|
6afdff0f29 | ||
|
|
c57ddf6aff | ||
|
|
dbaf8a165b | ||
|
|
3e1cc79a23 | ||
|
|
3b5befdaac | ||
|
|
fc6f002cb8 | ||
|
|
b8dd74299d | ||
|
|
8f91a4a097 | ||
|
|
d38ce5a119 | ||
|
|
0901f64118 | ||
|
|
63b00d46ee | ||
|
|
2602f044da | ||
|
|
a85cd94234 | ||
|
|
2ae97817a7 | ||
|
|
f237bdd4b7 | ||
|
|
822a304127 | ||
|
|
2718773b60 | ||
|
|
a1a75ddf0b | ||
|
|
3f61286cad | ||
|
|
9128953894 | ||
|
|
64c330459b | ||
|
|
1629609569 | ||
|
|
00af60b1df | ||
|
|
6e719c8fba | ||
|
|
ee3ddb2ef6 | ||
|
|
1d980afb62 | ||
|
|
c2bfc199e1 | ||
|
|
d6a1aa5009 | ||
|
|
a8fa4bd117 | ||
|
|
b32404ca5f | ||
|
|
c50b2562ab | ||
|
|
31c8973f93 | ||
|
|
4d1480eff3 | ||
|
|
9ed5a19bd0 | ||
|
|
c54a0b4f2a | ||
|
|
5ce79d9440 | ||
|
|
6235215c89 | ||
|
|
54139371f6 | ||
|
|
5547bb69e6 | ||
|
|
c370bd18c6 | ||
|
|
8cd6c7c9bc | ||
|
|
e97fb086cc | ||
|
|
aee742a8ac | ||
|
|
68d1271d5f | ||
|
|
ab73698a45 | ||
|
|
679b371eb8 | ||
|
|
c439732f9b | ||
|
|
b413bbb8ee | ||
|
|
1873740294 | ||
|
|
840de18a91 | ||
|
|
66c0aac3a1 | ||
|
|
8b10198b85 | ||
|
|
61c9a22fe1 | ||
|
|
bdf5980a1d | ||
|
|
901accb60c | ||
|
|
44fa30fdc3 | ||
|
|
4d5c7133bc | ||
|
|
b764c93e7c | ||
|
|
e362b04f0e | ||
|
|
ad2a6c7bc4 | ||
|
|
d9a13a6e30 | ||
|
|
cd750957c0 | ||
|
|
b228364a41 | ||
|
|
d66efcaa32 | ||
|
|
84f9deed77 | ||
|
|
878be3d019 | ||
|
|
5b305cc816 | ||
|
|
96ed9192a3 | ||
|
|
cf03ae0a5f | ||
|
|
8861424022 | ||
|
|
d51ba2b4fb | ||
|
|
6ea5069835 | ||
|
|
e4ab966d2f | ||
|
|
0c1eb13d93 | ||
|
|
e9f625f008 | ||
|
|
3d8a8664f2 | ||
|
|
49d286e7ea | ||
|
|
67db763ec9 | ||
|
|
aa067df380 | ||
|
|
d5009a7d0a | ||
|
|
2577b3208b | ||
|
|
d5ead2543c | ||
|
|
dfd86ed2e0 | ||
|
|
ccdb6d76bd | ||
|
|
7f1a08e16a | ||
|
|
ef456c66d7 | ||
|
|
29f5f44e1b | ||
|
|
85cb8526be | ||
|
|
af472ba833 | ||
|
|
e9a166f634 | ||
|
|
45315084ee | ||
|
|
df5b9de393 | ||
|
|
a5cdd69dbe | ||
|
|
4c47a6fd3b | ||
|
|
79eec2e751 | ||
|
|
3cdb1ab180 | ||
|
|
4f4a578c82 | ||
|
|
e7c479f702 | ||
|
|
85b5e96ea7 | ||
|
|
319e28ef3f | ||
|
|
d0094427eb | ||
|
|
af37be6389 | ||
|
|
decf6661fa | ||
|
|
d8a8614fb9 | ||
|
|
e357dfe37a | ||
|
|
3b1fb1c1a4 | ||
|
|
c7fd836760 | ||
|
|
7b3bab096e | ||
|
|
a367fcdbda | ||
|
|
6a262428cd | ||
|
|
89266f9aa2 | ||
|
|
1032ad3417 | ||
|
|
c8cdf2f1d2 | ||
|
|
cc375c0e40 | ||
|
|
d33724c1c9 | ||
|
|
70899994fe | ||
|
|
8bb801ac99 | ||
|
|
aaecb2b381 | ||
|
|
bbb955df1b | ||
|
|
2aa9026392 | ||
|
|
7cfbb92bb4 | ||
|
|
e1c01c6028 | ||
|
|
1ff1605cce | ||
|
|
bc0d86c771 | ||
|
|
9b26528226 | ||
|
|
4dad37c595 | ||
|
|
f5614333f5 | ||
|
|
14b9d2260f | ||
|
|
407128fd61 | ||
|
|
41faf42afe | ||
|
|
6cd5e44ad6 | ||
|
|
3203950310 | ||
|
|
5dc7a77e43 | ||
|
|
453d65a91d | ||
|
|
810b3a1a84 | ||
|
|
03429746af | ||
|
|
c2dbe993da | ||
|
|
3beeb1bef9 | ||
|
|
53db5ba473 | ||
|
|
b714f255fc | ||
|
|
7118dc95a5 | ||
|
|
4ea967f899 | ||
|
|
723c9006e1 | ||
|
|
84c4643e1b | ||
|
|
96b07af331 | ||
|
|
d8af0da440 | ||
|
|
d838f63454 | ||
|
|
32d4bf4c17 | ||
|
|
48966f3924 | ||
|
|
183f7fadf5 | ||
|
|
941ac30277 | ||
|
|
57d345c149 | ||
|
|
4ed966fefe | ||
|
|
3420cee9dc | ||
|
|
08caea4b9a | ||
|
|
1fcbd8923b | ||
|
|
637a468ec3 | ||
|
|
1546847c50 | ||
|
|
9e96025dc7 | ||
|
|
4dc94bba7d | ||
|
|
9e953bc3e1 | ||
|
|
26d661deca | ||
|
|
cfab334d0f | ||
|
|
e4c01285b2 | ||
|
|
954bbc6de4 | ||
|
|
7a7bd93639 | ||
|
|
3129a0441e | ||
|
|
e0cbb41bb9 | ||
|
|
3ecdad42a5 | ||
|
|
f45db909fd | ||
|
|
162c1d1eb4 | ||
|
|
fca7e7bec3 | ||
|
|
4d7f1d44c4 | ||
|
|
997d3726f0 | ||
|
|
ef69919644 | ||
|
|
2429a88e95 | ||
|
|
8f63b55ff3 | ||
|
|
260bfe5423 | ||
|
|
6e1d0cf109 | ||
|
|
7788298b8c | ||
|
|
2949253cb2 | ||
|
|
4ae88ad3b9 | ||
|
|
8b60c64ade | ||
|
|
93d6d1279e | ||
|
|
2b46fa6c56 | ||
|
|
191ad1e224 | ||
|
|
301a784e95 | ||
|
|
cd05eba183 | ||
|
|
82eafe2900 | ||
|
|
f428fcb871 | ||
|
|
9b74d0a4f4 | ||
|
|
a9eddd184c | ||
|
|
6bb483bfd6 | ||
|
|
30eccab53c | ||
|
|
1c6fc8a178 | ||
|
|
4ff0d0a799 | ||
|
|
a035612da3 | ||
|
|
a5727a0c92 | ||
|
|
6800e93d7a | ||
|
|
4a86acf879 | ||
|
|
a9ed8ceb7a | ||
|
|
7e4a3a6831 | ||
|
|
29bc735094 | ||
|
|
71f2923ee1 | ||
|
|
c18ba12ca9 | ||
|
|
76eb5a9309 | ||
|
|
8931cceb97 | ||
|
|
9408b0e2d4 | ||
|
|
608e78f307 | ||
|
|
61f415ccee | ||
|
|
fce1a96736 | ||
|
|
bb4ab7ab92 | ||
|
|
9db9594a48 | ||
|
|
9fb63f268f | ||
|
|
31fe5a5141 | ||
|
|
99a8c7afb0 | ||
|
|
38d3f5f6da | ||
|
|
9bace16a45 | ||
|
|
58b6b9505d | ||
|
|
ea91c37489 | ||
|
|
c22329e86b | ||
|
|
c0f742bc3e | ||
|
|
40bcee6d22 | ||
|
|
32b785d790 | ||
|
|
2b65682960 | ||
|
|
92cfcca1d9 | ||
|
|
c5b59d98ef | ||
|
|
45acfb353d | ||
|
|
4a2b8ab1af | ||
|
|
64370462b9 | ||
|
|
799451ce49 | ||
|
|
cc7c3807dd | ||
|
|
341e8aa3e4 | ||
|
|
c835fc28f9 | ||
|
|
bf4070f7cb | ||
|
|
8771c1b147 | ||
|
|
9d2ef274b9 | ||
|
|
487646e2e3 | ||
|
|
fe89817c58 | ||
|
|
a521b95070 | ||
|
|
239d35a60a | ||
|
|
7328d222a0 | ||
|
|
cef6382e7a | ||
|
|
491495aa8a | ||
|
|
9291b54ef7 | ||
|
|
908369efff | ||
|
|
19ef282581 | ||
|
|
268d101189 | ||
|
|
e9e85d870d | ||
|
|
b28c6f5c79 | ||
|
|
f9c1a24b72 | ||
|
|
c96fd6783b | ||
|
|
b622028f7d | ||
|
|
fb215ed2e3 | ||
|
|
dd65935b80 | ||
|
|
82903071b8 | ||
|
|
ce97dda738 | ||
|
|
f7c38a413e | ||
|
|
eac55c2236 | ||
|
|
eaf31adaa8 | ||
|
|
d371dac84c | ||
|
|
0ffb134b8c | ||
|
|
ffdb7244c4 | ||
|
|
2c1d02940d | ||
|
|
0efc27993e | ||
|
|
236fe9f8eb | ||
|
|
3d0c66f0f6 | ||
|
|
c9b02a6470 | ||
|
|
fc3465c750 | ||
|
|
dc2689f2c1 | ||
|
|
f998681dc0 | ||
|
|
d2ada889dc | ||
|
|
5bf8bc3a75 | ||
|
|
e0f55d3e8c | ||
|
|
1b201086ba | ||
|
|
7bfc455960 | ||
|
|
87933d1f8b | ||
|
|
56e74f28be | ||
|
|
a6cb1267e8 | ||
|
|
f6ec1bc92c | ||
|
|
8e386f6a67 | ||
|
|
24e72d1e48 | ||
|
|
6d29cc85b0 | ||
|
|
ef1871f40d | ||
|
|
5038605549 | ||
|
|
15bb0f3924 | ||
|
|
2d6f89b77f | ||
|
|
fb728d322b | ||
|
|
bb2d443246 | ||
|
|
7a406786f6 | ||
|
|
2e57210623 | ||
|
|
b1d1cc6de3 | ||
|
|
ff274e4eac | ||
|
|
3824a108aa | ||
|
|
dd645f8822 | ||
|
|
a8c4bea53d | ||
|
|
7d7cbeeb71 | ||
|
|
765fd892bf | ||
|
|
59a8ff9dce | ||
|
|
f5250e0584 | ||
|
|
b6f87e5a1e | ||
|
|
7c1a639796 | ||
|
|
aa97e3cb54 | ||
|
|
f7a977ecc8 | ||
|
|
715709ce25 | ||
|
|
77051d2609 | ||
|
|
301cfb25d9 | ||
|
|
0b9672076d | ||
|
|
0fdc13666f | ||
|
|
bb8e86addf | ||
|
|
f3e0bd4907 | ||
|
|
bf69e91cae | ||
|
|
8e344ecf4b | ||
|
|
4b388475ce | ||
|
|
efc156689f | ||
|
|
749fbe1ed8 | ||
|
|
6ec81c5586 | ||
|
|
91afd69a95 | ||
|
|
6e8abc1c32 | ||
|
|
9b9ecfb913 | ||
|
|
ba8b2f33d0 | ||
|
|
065d09df42 | ||
|
|
2f377343f1 | ||
|
|
945b0f3001 | ||
|
|
b1e08744ff | ||
|
|
a447eb5f85 | ||
|
|
860e6ddc4a | ||
|
|
861f1e585e | ||
|
|
0cc6c0db79 | ||
|
|
e01683b387 | ||
|
|
fe29ad8c5e | ||
|
|
ec8202d008 | ||
|
|
89889eced1 | ||
|
|
a377381942 | ||
|
|
4ab5285156 | ||
|
|
55c9c18bd1 | ||
|
|
8d72354d3f | ||
|
|
3cc27c9d80 | ||
|
|
7d3888a28b | ||
|
|
579df7df7f | ||
|
|
a0e46f1994 | ||
|
|
6634c568b2 | ||
|
|
690ef74313 | ||
|
|
02810fb358 | ||
|
|
f70b417b41 | ||
|
|
e7cf4e6cb8 | ||
|
|
71eebc1441 | ||
|
|
aa267f52a2 | ||
|
|
86b0cec215 | ||
|
|
8265bbfeb5 | ||
|
|
2c803a745d | ||
|
|
cd11f92ed9 | ||
|
|
ead5ae0d82 | ||
|
|
89aa4a1de9 | ||
|
|
6e7bfd03ee | ||
|
|
a0429660f4 | ||
|
|
7218057ca2 | ||
|
|
11b015e516 | ||
|
|
55069b0216 | ||
|
|
9968b1885c | ||
|
|
a243e4e320 | ||
|
|
feb61a3fa7 | ||
|
|
e3c14876a6 | ||
|
|
b655b9959d | ||
|
|
6825a1f2e6 | ||
|
|
925ed3c821 | ||
|
|
b6f0d7b769 | ||
|
|
0df81e5aaf | ||
|
|
8e2cb187da | ||
|
|
13f0159c15 | ||
|
|
5ecbbb1abe | ||
|
|
fe2be38ebb | ||
|
|
7b7d06cee3 | ||
|
|
b5147181af | ||
|
|
1fd351b959 | ||
|
|
e541f0224c | ||
|
|
dd90c3a2eb | ||
|
|
cd6f4c909f | ||
|
|
50addd73dd | ||
|
|
8a447ecc4f | ||
|
|
dce50105c9 | ||
|
|
0ce987a20e | ||
|
|
03846adbfb | ||
|
|
886070755c | ||
|
|
1be460aec3 | ||
|
|
d0ed93aec8 | ||
|
|
0d8ca58a67 | ||
|
|
e3ed2c4e6d | ||
|
|
090cf414e7 | ||
|
|
b1473fc1a4 | ||
|
|
1f75d5fd41 | ||
|
|
7c96a12a3c | ||
|
|
c412371ee6 | ||
|
|
738f392270 | ||
|
|
ef73f3ca75 | ||
|
|
cdb5e8176d | ||
|
|
293edd0d6b | ||
|
|
4ad7d915e5 | ||
|
|
270712e07d | ||
|
|
1191957495 | ||
|
|
2a078651b9 | ||
|
|
952677ae13 | ||
|
|
b179b48edc | ||
|
|
98e0ea5ae2 | ||
|
|
5f920fd290 | ||
|
|
49a68df5e4 | ||
|
|
a434731729 | ||
|
|
b56d08a70e | ||
|
|
4f39dce206 | ||
|
|
8d5f5e4afb | ||
|
|
e65082b585 | ||
|
|
10a1a28d04 | ||
|
|
b59fa77dbd | ||
|
|
c9c7ad90ab | ||
|
|
272e5b6fd0 | ||
|
|
8e337c890e | ||
|
|
8037557db6 | ||
|
|
4e8014a177 | ||
|
|
7b0f9f61fc | ||
|
|
f3c884111d | ||
|
|
fc60dc125f | ||
|
|
d423a0307f | ||
|
|
ec01c74da1 | ||
|
|
60cd40d5e5 | ||
|
|
0ab99bef41 | ||
|
|
8fae90bb9d | ||
|
|
dd4a9f10cf | ||
|
|
b68b8a0292 | ||
|
|
cbb909bb3a | ||
|
|
f1f68658d2 | ||
|
|
dd09eac2a5 | ||
|
|
6996155cae | ||
|
|
6787e4916e | ||
|
|
0f417110cb | ||
|
|
45a69a1139 | ||
|
|
70a051a413 | ||
|
|
81df5a56e2 | ||
|
|
ad572f267a | ||
|
|
4e996c821e | ||
|
|
f5bdcc8ea5 | ||
|
|
189347ee39 | ||
|
|
51076634bd | ||
|
|
f7b4aa91ab | ||
|
|
49e722d9da | ||
|
|
c2df13bf24 | ||
|
|
632636ff0a | ||
|
|
048ccd6d65 | ||
|
|
9e857e6329 | ||
|
|
36ae851a8d | ||
|
|
3ca9edeee6 | ||
|
|
29869cfcda | ||
|
|
22cafe9ed0 | ||
|
|
c90f7878d7 | ||
|
|
c5fdd35078 | ||
|
|
681883a8f9 | ||
|
|
9b69851155 | ||
|
|
7a1ec09104 | ||
|
|
4d2f9facee | ||
|
|
64bf0c406b | ||
|
|
9e03c05bcc | ||
|
|
3038b4be86 | ||
|
|
1bf05df4a9 | ||
|
|
1a820b4dac | ||
|
|
5765c3c8c9 | ||
|
|
fa23eeb055 | ||
|
|
e1cad034a2 | ||
|
|
2a89743a3d | ||
|
|
955eace387 | ||
|
|
97d1effe1c | ||
|
|
8ba380fcbd | ||
|
|
8fdebc2a98 | ||
|
|
8381e32148 | ||
|
|
105d6b7964 | ||
|
|
e69f2be1f0 | ||
|
|
53be745737 | ||
|
|
fcaf6967fa | ||
|
|
65419b1b4b | ||
|
|
04bc6c8975 | ||
|
|
85dbab14f9 | ||
|
|
8f00e35d1e | ||
|
|
68bcfadd42 | ||
|
|
2403d0adfb | ||
|
|
731544492e | ||
|
|
d4d5b7a14d | ||
|
|
1357b17b64 | ||
|
|
495719c366 | ||
|
|
ee79bdcfc8 | ||
|
|
2e786e63bf | ||
|
|
eb9ec21c89 | ||
|
|
cde7e4a5e9 | ||
|
|
7ce2587bdb | ||
|
|
e7d1309c76 | ||
|
|
663cee8fbe | ||
|
|
5cbe89632f | ||
|
|
c2423605cc | ||
|
|
d338e3889a | ||
|
|
b6db683f30 | ||
|
|
c239c4f552 | ||
|
|
8e6eb63c9e | ||
|
|
54c1757423 | ||
|
|
902a9bdc42 | ||
|
|
08ed86fb18 | ||
|
|
daa0070e08 | ||
|
|
3c07e5ad8b | ||
|
|
472e332d05 | ||
|
|
244f440cb8 | ||
|
|
bd5d489ac2 | ||
|
|
53e910d549 | ||
|
|
ab4a9370e7 | ||
|
|
5f4fa61310 | ||
|
|
c0789858a1 | ||
|
|
aa0ff732df | ||
|
|
1b2e8d218d | ||
|
|
d70085c606 | ||
|
|
9971a560e1 | ||
|
|
209750386e | ||
|
|
6ec5b9abf6 | ||
|
|
f52e8ebfa1 | ||
|
|
4645b8338b | ||
|
|
42ce869b08 | ||
|
|
ca2aaa1221 | ||
|
|
8edcb25eee | ||
|
|
3051971db7 | ||
|
|
dae9db7042 | ||
|
|
8e649501cf | ||
|
|
5dbcbb7489 | ||
|
|
697b105a10 | ||
|
|
43a974af83 | ||
|
|
610814f0d0 | ||
|
|
e9109b0b49 | ||
|
|
166df0104e | ||
|
|
490ab65165 | ||
|
|
78e94df9c5 | ||
|
|
5be1cf2973 | ||
|
|
83e4eb16b1 | ||
|
|
e84eedd66f | ||
|
|
e6a3c63dd5 | ||
|
|
b4c970a005 | ||
|
|
e725ae097d | ||
|
|
5827eb33c5 | ||
|
|
061e9a1812 | ||
|
|
00e846b49b | ||
|
|
9ca928c6e1 | ||
|
|
189593af75 | ||
|
|
7ef61b6d3c | ||
|
|
60962713f4 | ||
|
|
b8be50a592 | ||
|
|
4a6f7050cd | ||
|
|
1d7109ddf9 | ||
|
|
41c22b2587 | ||
|
|
b2e2c11af8 | ||
|
|
592a52e9a2 | ||
|
|
d4645496f1 | ||
|
|
7e24e129f6 | ||
|
|
df56b84ad2 | ||
|
|
a0a7ae9be1 | ||
|
|
d5e1551839 | ||
|
|
82e887b7a8 | ||
|
|
b1c9e9a87e | ||
|
|
6cb837d2b7 | ||
|
|
d856d4c920 | ||
|
|
1a17ec08ee | ||
|
|
e5c694bae9 | ||
|
|
f3fc1e4a2b | ||
|
|
45ef9222fe | ||
|
|
c282f38726 | ||
|
|
227eaeae75 | ||
|
|
4dcb8d2ca0 | ||
|
|
07a95532be | ||
|
|
2f2e06c99e | ||
|
|
5010806c4d | ||
|
|
a9d72fbef0 | ||
|
|
82fea2a4ea | ||
|
|
aa33f74f5d | ||
|
|
e193c4ef47 | ||
|
|
65a3e4feb2 | ||
|
|
82d4933d74 | ||
|
|
ce092fcded | ||
|
|
ecfd4b3d66 | ||
|
|
f636951d3c | ||
|
|
07042a7d4f | ||
|
|
1c56622d89 | ||
|
|
60c88beb29 | ||
|
|
dc02601db1 | ||
|
|
ff88e20fa3 | ||
|
|
9f3b4c48ea | ||
|
|
f1df9fe243 | ||
|
|
de583caa80 | ||
|
|
004cd05973 | ||
|
|
b86ffd0c3d | ||
|
|
5ae4353a70 | ||
|
|
79e98c2f6c | ||
|
|
02d8d4ca4a | ||
|
|
532e12ec83 | ||
|
|
ebe8d8b0b9 | ||
|
|
aa040f3a6c | ||
|
|
0b63db46db | ||
|
|
95e303d9ba | ||
|
|
b5c992afd9 | ||
|
|
40c921c7f5 | ||
|
|
24b87bed9f | ||
|
|
4a40f4edc7 | ||
|
|
9308d1b689 | ||
|
|
d3e6bb3444 | ||
|
|
662e637758 | ||
|
|
6ecf27e168 | ||
|
|
c5b1f2870f | ||
|
|
d67efc320d | ||
|
|
1280d3ba74 | ||
|
|
f828e4a641 | ||
|
|
9efe15a699 | ||
|
|
0f0d24a7ec | ||
|
|
0a63576a6a | ||
|
|
46daf57cac | ||
|
|
c59e9efabc | ||
|
|
12d94b3e60 | ||
|
|
d5388ba7de | ||
|
|
bd4fd73417 | ||
|
|
72b953b354 | ||
|
|
93d61a7ac5 | ||
|
|
5a2aa8b42b | ||
|
|
ce398d26c3 | ||
|
|
2e5a7b6a86 | ||
|
|
6f09136f73 | ||
|
|
1f660d6694 | ||
|
|
01681c3af2 | ||
|
|
168941a0ad | ||
|
|
ebc0e60cc9 | ||
|
|
98ef0dfcb1 | ||
|
|
db2ffadb43 | ||
|
|
57bb9665c6 | ||
|
|
a1fbd10561 | ||
|
|
454ae1bf95 | ||
|
|
d2b4d6c837 | ||
|
|
61201a6573 | ||
|
|
e084495814 | ||
|
|
659f95ccd3 | ||
|
|
ff198d4728 | ||
|
|
2135fd8656 | ||
|
|
e38c0d2ff8 | ||
|
|
c73bfcb1ea | ||
|
|
6f21c0c34b | ||
|
|
5cddae5ba9 | ||
|
|
f99eae6b5a | ||
|
|
a2b61558d3 | ||
|
|
f8ccb97d60 | ||
|
|
9973e9dbbb | ||
|
|
90ed8c0e0a | ||
|
|
c77a9d1747 | ||
|
|
df700d80e4 | ||
|
|
33b8b94f22 | ||
|
|
b9f8a8b4b8 | ||
|
|
84b48eac60 | ||
|
|
67803cfdd3 | ||
|
|
b5157ad8dc | ||
|
|
55de179730 | ||
|
|
c0edd15edb | ||
|
|
43f4228e19 | ||
|
|
99762dce94 | ||
|
|
45dcd7f21f | ||
|
|
8ab9590594 | ||
|
|
b9371805e2 | ||
|
|
2d18153b1d | ||
|
|
62e35ec006 | ||
|
|
e0b93fbd49 | ||
|
|
4aa17930fd | ||
|
|
aadb567712 | ||
|
|
f3230fc59e | ||
|
|
dc5ae91bb2 | ||
|
|
f747091257 | ||
|
|
d0a76cf665 | ||
|
|
c6cc6d34e1 | ||
|
|
938024fea2 | ||
|
|
08ce55e29e | ||
|
|
69c7cdb49f | ||
|
|
f4439974e4 | ||
|
|
8d1f59cf46 | ||
|
|
e80daf2795 | ||
|
|
131d9f689c | ||
|
|
d7c12362ac | ||
|
|
384cc8305d | ||
|
|
71d643260c | ||
|
|
c23756f546 | ||
|
|
ee47575431 | ||
|
|
ad98cb0d45 | ||
|
|
03beeac049 | ||
|
|
6a5c0bd8ee | ||
|
|
807eb59192 | ||
|
|
418cc7bf86 | ||
|
|
f429037d63 | ||
|
|
6ef7411999 | ||
|
|
108e2f579d | ||
|
|
51e30c1517 | ||
|
|
c9884e512b | ||
|
|
7c8e3de261 | ||
|
|
a5c6b1135c | ||
|
|
e76c23b096 | ||
|
|
627914abda | ||
|
|
2e9b8c1360 | ||
|
|
d6fe562067 | ||
|
|
4cd86d0daf | ||
|
|
58b046254f | ||
|
|
39bae68b1c | ||
|
|
777b9bef1b | ||
|
|
35cf441d91 | ||
|
|
25d0309ff3 | ||
|
|
8032566f44 | ||
|
|
d9242ddd5b | ||
|
|
0a54fe6db8 | ||
|
|
c4509d726a | ||
|
|
af906740ca | ||
|
|
3f58676849 | ||
|
|
6d5e9aba39 | ||
|
|
0f238286a3 | ||
|
|
245e2a353e | ||
|
|
e56b26146a | ||
|
|
b151341148 | ||
|
|
6965459f5d | ||
|
|
f7f442c9d7 | ||
|
|
b79fb373a8 | ||
|
|
b0db9ecf78 | ||
|
|
df780124d7 | ||
|
|
e8e161098f | ||
|
|
83fe977a85 | ||
|
|
5618de2cd3 | ||
|
|
00e6aced53 | ||
|
|
7eb0d13590 | ||
|
|
820852e08b | ||
|
|
d3038fe49b | ||
|
|
e1b6288860 | ||
|
|
99d285335e | ||
|
|
08f124b6f9 | ||
|
|
734a72d7a0 | ||
|
|
ec8c504f28 | ||
|
|
dbc47332ee | ||
|
|
4e65c39b36 | ||
|
|
3f356f9de8 | ||
|
|
2c435ee9d9 | ||
|
|
30f9c3d290 | ||
|
|
d60d5c928c | ||
|
|
61cf79de69 | ||
|
|
671ac08a04 | ||
|
|
40fd894cce | ||
|
|
1988a9d5d7 | ||
|
|
80a5d26dce | ||
|
|
a55dd18528 | ||
|
|
c41f6033be | ||
|
|
8b37302164 | ||
|
|
ebaf0c990f | ||
|
|
5cc39930e5 | ||
|
|
b1bfef8784 | ||
|
|
f9bb1024a7 | ||
|
|
88c45a9c45 | ||
|
|
c19618eddc | ||
|
|
b11597b0de | ||
|
|
2fed92e69a | ||
|
|
4fde09e0cc | ||
|
|
fd280aee1a | ||
|
|
4deb116076 | ||
|
|
89181eb74e | ||
|
|
f72ec97b37 | ||
|
|
b7fa4b8b05 | ||
|
|
7037793369 | ||
|
|
14b95d3ba0 | ||
|
|
769b966806 | ||
|
|
39bec9f631 | ||
|
|
17fa3399ff | ||
|
|
bc68b924e5 | ||
|
|
7e009516e4 | ||
|
|
b565ec5d6c | ||
|
|
62f5376c11 | ||
|
|
fd4aae3b30 | ||
|
|
6917813e2a | ||
|
|
0a5495f185 | ||
|
|
21c316293b | ||
|
|
2083ecbc27 | ||
|
|
29bd8d9ce9 | ||
|
|
89c3c1b272 | ||
|
|
da1025a625 | ||
|
|
f38492a920 | ||
|
|
80b73e7aba | ||
|
|
44f7fae2d4 | ||
|
|
ecaea51a1c | ||
|
|
301135d006 | ||
|
|
3bf1d6c6ae | ||
|
|
e26e342dd7 | ||
|
|
5048080ff6 | ||
|
|
cad896f909 | ||
|
|
d6d33c6a22 | ||
|
|
8db457bd42 | ||
|
|
903e0d5b0b | ||
|
|
6b2f3d05d5 |
+19
-7
@@ -1,7 +1,9 @@
|
||||
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
||||
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
|
||||
# uvicorn variable, allow https behind a proxy
|
||||
# uvicorn variable, uncomment to allow https behind a proxy
|
||||
# FORWARDED_ALLOW_IPS="*"
|
||||
|
||||
DEBUG=false
|
||||
@@ -11,11 +13,12 @@ LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Restricts access, User IDs seperated by comma
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||
# Warning: Enabling this will make LNbits ignore this configuration file. Your 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
|
||||
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
@@ -53,8 +56,9 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet, LnTipsWallet
|
||||
# LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
|
||||
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet,
|
||||
# LndWallet, LndRestWallet, CoreLightningWallet, EclairWallet,
|
||||
# LnTipsWallet, LNbitsWallet, SparkWallet, FakeWallet,
|
||||
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.
|
||||
@@ -75,6 +79,14 @@ CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_KEY=LNBITS_ADMIN_KEY
|
||||
|
||||
# LndWallet
|
||||
LND_GRPC_ENDPOINT=127.0.0.1
|
||||
LND_GRPC_PORT=10009
|
||||
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
|
||||
# To use an AES-encrypted macaroon, set
|
||||
# LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn"
|
||||
|
||||
# LndRestWallet
|
||||
LND_REST_ENDPOINT=https://127.0.0.1:8080/
|
||||
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -24,7 +24,9 @@ jobs:
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install packages
|
||||
run: poetry install
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Check black
|
||||
run: make checkblack
|
||||
- name: Check isort
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
sudo apt install unzip
|
||||
- name: Run migrations
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
run: poetry run mypy
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
sudo chmod -R a+rwx .
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
env:
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -72,6 +73,7 @@ jobs:
|
||||
sudo chmod -R a+rwx .
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
env:
|
||||
@@ -95,7 +97,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -116,6 +118,7 @@ jobs:
|
||||
sudo chmod -R a+rwx .
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
env:
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -44,6 +44,7 @@ jobs:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
run: make test
|
||||
@@ -67,7 +68,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
poetry-version: ["1.3.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install
|
||||
- name: Run tests
|
||||
env:
|
||||
|
||||
+1
-1
@@ -7,6 +7,6 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ format: prettier isort black
|
||||
|
||||
check: mypy checkprettier checkisort checkblack
|
||||
|
||||
prettier: $(shell find lnbits -name "*.js" -name ".html")
|
||||
prettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||
|
||||
pyright:
|
||||
./node_modules/.bin/pyright
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
|
||||
@@ -18,7 +21,7 @@ mypy:
|
||||
isort:
|
||||
poetry run isort .
|
||||
|
||||
checkprettier: $(shell find lnbits -name "*.js" -name ".html")
|
||||
checkprettier: $(shell find lnbits -name "*.js" -o -name ".html")
|
||||
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
|
||||
|
||||
checkblack:
|
||||
|
||||
@@ -7,29 +7,29 @@ LNbits
|
||||
|
||||

|
||||
|
||||
# LNbits v0.9 BETA, free and open-source lightning-network wallet/accounts system
|
||||
# LNbits v0.9 BETA, free and open-source Lightning wallet accounts system
|
||||
|
||||
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
|
||||
|
||||
(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
|
||||
LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me
|
||||
|
||||
Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits server!
|
||||
|
||||
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
|
||||
LNbits is a Python server that sits on top of any funding source. It 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
|
||||
* 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
|
||||
|
||||
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
|
||||
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, lntxbot, LightningTipBot, and with more being added regularly.
|
||||
|
||||
See [legend.lnbits.org](https://legend.lnbits.org) for more detailed documentation.
|
||||
See [docs.lnbits.org](https://docs.lnbits.org) for more detailed documentation.
|
||||
|
||||
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
|
||||
|
||||
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 excellent funding sources for LNbits.
|
||||
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.
|
||||
|
||||
## Running LNbits
|
||||
|
||||
@@ -58,19 +58,18 @@ Example use would be an ATM, which utilises LNURL, if the user scans the QR with
|
||||
|
||||

|
||||
|
||||
## LNbits as an insta-wallet
|
||||
## LNbits as an instant wallet
|
||||
|
||||
Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
|
||||
"Go to this website", has a lot less friction than "Download this app".
|
||||
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".
|
||||
|
||||

|
||||
|
||||
## Tip us
|
||||
|
||||
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||
If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
|
||||
|
||||
|
||||
[docs]: https://legend.lnbits.org/
|
||||
[docs]: https://docs.lnbits.org/
|
||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
|
||||
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
legend.lnbits.org
|
||||
docs.lnbits.org
|
||||
+1
-1
@@ -3,7 +3,7 @@ remote_theme: pmarsceill/just-the-docs
|
||||
color_scheme: dark
|
||||
logo: "/logos/lnbits-full--inverse.png"
|
||||
search_enabled: true
|
||||
url: https://legend.lnbits.org
|
||||
url: https://docs.lnbits.org
|
||||
aux_links:
|
||||
"LNbits on GitHub":
|
||||
- "//github.com/lnbits/lnbits"
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ nav_order: 3
|
||||
API reference
|
||||
=============
|
||||
|
||||
[Swagger Docs](https://legend.lnbits.org/devs/swagger.html)
|
||||
[Swagger Docs](https://docs.lnbits.org/devs/swagger.html)
|
||||
|
||||
@@ -28,7 +28,9 @@ Going over the example extension's structure:
|
||||
Adding new dependencies
|
||||
-----------------------
|
||||
|
||||
If for some reason your extensions needs a new python package to work, you can add a new package using `venv`, or `poerty`:
|
||||
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 `venv`, or `poerty`:
|
||||
|
||||
```sh
|
||||
$ poetry add <package>
|
||||
@@ -37,8 +39,7 @@ $ ./venv/bin/pip install <package>
|
||||
```
|
||||
|
||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
||||
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry`.
|
||||
`nix` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||
|
||||
|
||||
SQLite to PostgreSQL migration
|
||||
|
||||
@@ -40,3 +40,33 @@ Allowed Users
|
||||
=============
|
||||
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
|
||||
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
|
||||
|
||||
|
||||
How to activate
|
||||
=============
|
||||
```
|
||||
$ sudo systemctl stop lnbits.service
|
||||
$ cd ~/lnbits-legend
|
||||
$ sudo nano .env
|
||||
```
|
||||
-> set: `LNBITS_ADMIN_UI=true`
|
||||
|
||||
Now start LNbits once in the terminal window
|
||||
```
|
||||
$ poetry run lnbits
|
||||
```
|
||||
It will now show you the Super User Account:
|
||||
|
||||
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
|
||||
|
||||
The `/wallet?usr=..` is your super user account. 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`
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
layout: default
|
||||
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>
|
||||
<p>If you want to send sats from the same node that is the funding source of your LNbits, you will need to edit the lnd.conf file. The parameters to be included are:</p>
|
||||
|
||||
```
|
||||
allow-circular-route=1
|
||||
allow-self-payment=1
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Funding source only available via tor (e.g. Umbrel)</summary>
|
||||
<p>If you want your setup to stay behind tor then only apps, pos and wallets that have tor activated can communicate with your wallets. Most likely you will have trouble when people try to redeem your voucher through onion or when importing your lnbits wallets into a wallet-app that doesnt support tor. If you plan to let LNbits wallets interact with plain internet shops and services you should consider <a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">setting up hybrid mode for your node</a>.</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Funding source is in a cloud</summary>
|
||||
<p>This means that you might not have access to some files which would allow certain administrative functions. E.g. on <a href="https://voltage.cloud/">Voltage</a> lnd.conf can not be edited. Payments from your node to LNbits wallets can therefore not be configurated in this case atm so you will need to take an extra wallet to send from funding source->wallet x->LNbits wallet (only) for the initial funding of the wallet.</p>
|
||||
</details>
|
||||
|
||||
<details><summary>LNbits via clearnet domain</summary>
|
||||
<p><a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">Step by step guide how to convert your Tor only node</a> into a clearnet node to make apps like LNbits accessible via https.</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Which funding sources can I use for LNbits?</summary>
|
||||
<p>There are several ways to run a LNbits instance funded from different sources. It is importan to choose a source that has a good liquidity and good peers connected. If you use LNbits for public services your users´ payments can then flow happily in both directions. If you would like to fund your LNbits wallet via btc please see section Troubleshooting.</p>
|
||||
<p>The <a href="http://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each: CLN, LND, LNPay, Cliche, OpenNode as well as bots.</p>
|
||||
</details>
|
||||
|
||||
<!--Later to be added
|
||||
<details><summary>Advanced setup options</summary>
|
||||
<p>more text coming soon...</p>
|
||||
</details>
|
||||
-->
|
||||
|
||||
<details><summary>Can I prevent others from generating wallets on my node?</summary>
|
||||
<p>When you run your LNbits in clearnet basically everyone can generate a wallet on it. Since the funds of your node are bound to these wallets you might want to prevent that. There are two ways to do so:</p>
|
||||
<ul>
|
||||
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/blob/main/.env.example">in the .env file</a></li>
|
||||
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/usermanager">via the Usermanager-Extension</a>. You can find <a href="http://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
|
||||
</ul>
|
||||
<p>Please note that all entries in the .env file will not be the taken into account once you activated the admin extension.</p>
|
||||
</details>
|
||||
</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>
|
||||
<li>
|
||||
<details><summary>LNbits is running via Tor only, you can't open it on a public domain like lnbits.yourdomain.com</summary>
|
||||
<ul>
|
||||
<li>Given that you want your setup to stay like this open your LNbits wallet using the .onion URI and create it again. In this way the QR is generated to be accessible via this .onion URI so via tor only. Do not generate that QR from a .local URI, because it will not be reachable via internet - only from within your home-LAN.</li>
|
||||
<li>Open your LN wallet app that you used to scan that QR and this time by using tor (see wallet app settings).
|
||||
If the app doesn't offer tor, you can use Orbot (Android) instead. See section Installation->Clearnet for detailed instructions.</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details><summary>LNbits is running via Tor only, you want to offer public LN services via https</summary>
|
||||
<ul>
|
||||
<li>For this we need to partially open LNbits to a clearnet (domain/IP) through a https SSL certificate. Follow the instructions from <a href="https://docs.lnbits.org/guide/installation.html#reverse-proxy-with-automatic-https-using-caddy">this LNbits caddy installation instruction</a>.
|
||||
You need to have a domain and to be able to configure a CNAME for your DNS record as well as generate a subdomain dedicated to your LNbits instance like eg. lnbits.yourdomain.com.
|
||||
You also need access to your internet router to open the https port (usually 443) and forward it your LNbits IP within your LAN (usually 80). The ports might depend on your node implementation if those ports do not work please ask for them in a help group of your node supplier.</li>
|
||||
<li>You can also follow the Apache installation option, explained in the <a href="https://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https">LNbits installation manual</a>.</li>
|
||||
<li>If you run LNbits from a bundle node (Umbrel, myNode, Embassy, Raspiblitz etc), you can follow <a href="https://github.com/TrezorHannes/vps-lnbits">this extensive guide</a> with many options to switch your Tor only LNbits into a clearnet LNbits. For Citadel there is a HTTPS Option in your manual to activate https for LNbits in the newest version.</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details><summary>Wallet-URL deleted, are my funds safu ?</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<details><summary>Wallet on demo server legend.lnbits</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>
|
||||
<li>
|
||||
<details><summary>Wallet on your own funding source/node</summary>
|
||||
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place.
|
||||
You can find all LNbits users and wallet-IDs in your LNbits user manager extension or in your sqlite database.
|
||||
To edit or read the LNbits database, go to the LNbits /data folder and look for the file called sqlite.db.
|
||||
You can open and edit it with excel or with a dedicated SQL-Editor like <a href="https://sqlitebrowser.org/">SQLite browser</a>.</p>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<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,
|
||||
the comment box will be displayed in the payment process. You can also edit a LNURL-p already created and add that number.</p>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Can I deposit onchain BTC to LNbits ?</summary>
|
||||
<p>There are multiple ways to exchange sats from onchain btc to LN btc (resp. to LNbits).</p>
|
||||
<ul>
|
||||
<li>
|
||||
<details><summary>A - Via an external swap service</summary>
|
||||
<p>If the user do not have full acceess of your LNbits, is just an external user, can use swap services like <a href="https://boltz.exchange/">Boltz</a>, <a href="https://fixedfloat.com/">FixedFloat</a>, <a href="https://swap.diamondhands.technology/">DiamondHands</a> or <a href="https://zigzag.io/">ZigZag</a>.</p>
|
||||
<p>This is useful if you provide only LNURL/LN invoices from your LNbits instance, but a payer only has onchain sats so
|
||||
they will have to the swap those sats first on their side.</p>
|
||||
<p>The procedure is simple: user sends onchain btc to the swap service and provides the LNURL / LN invoice from LNbits as destination of the swap.</p>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details><summary>B - Using the Onchain LNbits extension</summary>
|
||||
<p>Keep in mind that this is a separate wallet, not the LN btc one that is represented by LNbits as "your wallet" upon your LN funding source.
|
||||
This onchain wallet can be used also to swap LN btc to (e.g. your hardwarewallet) by using the LNbits Boltz or Deezy extension.
|
||||
If you run a webshop that is linked to your LNbits for LN payments, it is very handy to regularily drain all the sats from LN into onchain.
|
||||
This leads to more space in your LN channels to be able to receive new fresh sats.</p>
|
||||
<p>Procedure:</p>
|
||||
<ul>
|
||||
<li>Use Electrum or Sparrow wallet to create a new onchain wallet and save the backup seed in a safe place</li>
|
||||
<li>Go to wallet information and copy the xpub</li>
|
||||
<li>Go to LNbits - Onchain extension and create a new watch-only wallet with that xpub</li>
|
||||
<li>Go to LNbits - Tipjar extension and create a new Tipjar. Select also the onchain option besides the LN wallet.</li>
|
||||
<li>Optional - Go to LNbits - SatsPay extension and create a new charge for onchain btc.
|
||||
You can choose between onchain and LN or both. It will then create an invoice that can be shared.</li>
|
||||
<li>Optional - If you use your LNbits linked to a Wordpress + Woocommerce page, once you create/link a watch-only wallet to your LN btc shop wallet,
|
||||
the customer will have both options to pay on the same screen.</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details><summary>Where can I see payment details?</summary>
|
||||
<p>When you receive a payment in LNbits, the transaction log will display only a resumed type of the transaction.
|
||||
|
||||

|
||||
|
||||
<p>In your transaction overview you will find a little green arrow for received and a red arrow for sended funds.<p>
|
||||
<p>If you click on those arrows, a details popup shows attached messages as well as the sender´s name if given.</p>
|
||||
</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>
|
||||
</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 LNDHUB extension.</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>
|
||||
<p>You can use <a href="https://zeusln.app">Zeus</a> or <a href="https://bluewallet.io">Bluewallet</a> as wallet apps for a lndhub account.</p>
|
||||
<p>Keep in mind: if your LNbits instance is Tor only, you must use also theose apps behind Tor and open the LNbits page through your Tor .onion address.</p>
|
||||
</details>
|
||||
</ul>
|
||||
</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>
|
||||
<p>This is a do-it-yourself project consisting of a mini-computer (Raspberry Pi Zero), a coin acceptor, a display, a 3D printed case, and a Bitcoin Lightning wallet as a funding source. It exchanges fiat coins for valuable Bitcoin Lightning ⚡ Satoshis. The user can pick up the Satoshis via QR code (LNURL-withdraw) on a mobile phone wallet. It works based on BTCPayer server, LNTXBOT is not longer an option. You can get the components as individual parts and build the case yourself e.g. from <a href="https://www.Fulmo.org">Fulmo</a> who also made a <a href="https://blog.fulmo.org/the-lightningatm-pocket-edition/">guide</a> on it. The shop offers payments in Bitcoin and Lightning ⚡. The code can be found on <a href="https://github.com/21isenough/LightningATM">the ATM github project page></a>.</p>
|
||||
</details>
|
||||
|
||||
<details><summary>POS Terminal - an offline terminal for merchants</summary>
|
||||
<p>The LNpos is a self-sufficient point of sale terminal which allows offline onchain payments and an offline Lightning ATM for withdrawals. Free and open source software, free from intermediaries, with integrated battery, WLAN, DIY. You can get the 3D print as well as the whole kit from the LNbits shop from 👇 Ressources. It allows
|
||||
<li>LNPoS Online interactive Lightning payments</li>
|
||||
<li>LNURLPoS Offline Lightning Payments. Passive interaction, sharing a secret as evidence</li>
|
||||
<li>OnChain For onchain payments. Generates an address and displays a link for verification</li>
|
||||
<li>LNURLATM Offline Lightning Payouts. Generates LNURLw link to do withdrawals</li>
|
||||
<p>
|
||||
<img width="285" alt="Bildschirmfoto 2023-01-20 um 18 09 34" src="https://user-images.githubusercontent.com/63317640/213761202-4c4d8531-7184-4e53-8645-fe0f08ac7d17.png">
|
||||
</p>
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Hardware Wallet- build your own, stack harder</summary>
|
||||
<p>The hardwarewallet is a very cheap solution for builders. The projects´ <a hrel="https://github.com/lnbits/hardware-wallet">code and installation instructions for the LNbits hardware wallet can be found on github</a></p>
|
||||
<p>
|
||||
<img width="546" alt="Bildschirmfoto 2023-01-20 um 18 08 37" src="https://user-images.githubusercontent.com/63317640/213760948-38fd77b0-9247-4505-9433-f5af1b223527.png">
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Bitcoin Switch - turn things on with bitcoin</summary>
|
||||
<p>Candy dispenser, vending machines (online), grabbing machines, jukeboxes, bandits and <a href="https://github.com/cryptoteun/awesome-lnbits">all sorts of other things have already been build with LNbits´ tools</a>. Further info see 👇 Ressources.</p>
|
||||
<p>
|
||||
<img width="549" alt="Bildschirmfoto 2023-01-20 um 18 11 55" src="https://user-images.githubusercontent.com/63317640/213761646-d25d4745-e50d-4389-98e5-f83237a8cf6b.png">
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Vending machine (offline)</summary>
|
||||
<p>This code works similar to the LNpos. Note that the <a href=" https://www.youtube.com/watch?v=Fg0UuuzsYXc&t=762s">setup-video for the vending machine</a> misses the new way of installing it via the new LNURLdevices extension. The <a href="https://github.com/arcbtc/LNURLVend">vending machine project code resides on github</a>.</p>
|
||||
<p>
|
||||
<img width="753" alt="Bildschirmfoto 2023-01-20 um 18 13 22" src="https://user-images.githubusercontent.com/63317640/213761946-5025a7b8-c6d4-40cf-a6d3-d298593e79f6.png">
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details><summary><b>Resources - Building hardware tools</b></summary>
|
||||
<ul>
|
||||
<li><a href="https://t.me/makerbits'">MakerBits</a> - Telegram support group</li>
|
||||
<li><a href="https://ereignishorizont.xyz/">Instructions for LNpos, Switch, ATM, BTCticker</a> - guides in DE & EN</li>
|
||||
<li><a href="https://shop.lnbits.com/">LNbits shop</a> - eadymade hardware gimmicks from the community</li>
|
||||
<li><a href="https://github.com/cryptoteun/awesome-lnbits#hardware-projects-utilizing-lnbits">Collection of hardware projects using LNbits</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</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>
|
||||
</details>
|
||||
|
||||
<details><summary>Swapping ⚡️LN BTC to a BTC address</summary>
|
||||
<p>LNbits has two swap extensions integrated: <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz">Boltz</a> and <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/deezy">Deezy</a>.</p>
|
||||
<p>For a merchant that uses LNbits to receive BTC payments through LN, this is very handy to move the received sats from LN channels into onchain wallets. It not only helps you HODLing but is also freeing up "space in your channels" so you are ready to receive more sats.</p>
|
||||
<p>Boltz has an option to setup an automated swap triggered by a certain amount received.</p>
|
||||
</details>
|
||||
|
||||
<details><summary>Voucher</summary>
|
||||
<p>Printed voucher links or tippingcards</p>
|
||||
<p>To generate voucher you will need LNbits to be available in clearnet. Please consider running your own LNbits instance for this.</p>
|
||||
<p>LNURLw are strings that represent a faucet-link to a wallet. By scanning it, everyone will be able to withdraw sats from it. A LNURLw can be either a QR that leads to a static link or to one that responds with new invoices every time it is scanned (click "no assmilking"). You can create these QR by adding the LNURLw extension and generate the vouchertype you need.</p>
|
||||
<ul>
|
||||
<li>Voucher can as well be printed directly from LNbits. After you created it, click the "eye" next to the link. By pressing the printer-button you print the plain QR but you could as well integrate it into a nice tippincard or voucher template by choosing "Advanced voucher" -> "Use custom voucher design". We collected some designs as well as templates to make your own ones under <a href="https://youtu.be/c5EV9UNgVqk">this LNbits voucher video-guide.</a>. You will be able to create and print as much voucher as you like with it. Happy orangepilling!</li>
|
||||
<li> Note that your LNbits needs to be reachable in clearnet to offer vouchers to others.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details><summary>NFC Cards, Badges, Rings etc.</summary>
|
||||
<p>Creating a NFC card for a wallet</p>
|
||||
<p>To generate links for your cards you will need LNbits to be available in clearnet. Please consider running your own LNbits instance for this.</p>
|
||||
<ul>
|
||||
<li>On top to just printing voucher for your wallet you can also <a href="https://youtu.be/CQz1ILcK0PY">write these LNURLw to a simple NFC card fromon NTAG216</a> by not clicking the printer but the NFC symbol on android/chrome and tapping your card against the device. This will enable the cardholder to directly spend those sats at a tpos, pos or wallet-app another one uses that can handle lightning payments via NFC. </li>
|
||||
<li>If you run an event and want to hand out bigger amounts of cards with simple voucher links on consider this <a hrel="nfc-brrr.com/">NFC-brrr batch tool</a> as well as using NTAG424 cards, so that your customers can rewrite them later with an own wallet and the boltcard service (see ff)</li>
|
||||
<li>For bigger amounts the Boltcard-Extension should be used. It will generate a link that sends a new invoice every time it is used for payments and keeps track too if the allowed card-ID is redeeming funds. Hence the setup of Boltcards is a bit safer but it needs some additional tools. You can find <a href="https://plebtag.com/write-tags/">further infos on creating or updating boltcards here</a>.</li>
|
||||
</ul><p>
|
||||
<ul><details><summary>Resources - NFC & LNbits</summary>
|
||||
<ul>
|
||||
<li><a href="https://www.boltcard.org">Coincorner Boltcard</a></li>
|
||||
<li><a href="https://www.plebtag.com">PlebTag (infos, Lasercards, Badges)</a></li>
|
||||
<li><a href="https://www.lasereyes.cards">Lasercards</a></li>
|
||||
<li><a href="https://www.bitcoin-ring.com">Bitcoin Ring</a></li>
|
||||
<li><a href="https://github.com/taxmeifyoucan/HCPP2021-Badge">Badge</a></li>
|
||||
<li><a href="https://github.com/cryptoteun/awesome-lnbits#powered-by-lnbits">Powered by LNbits examples</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</p>
|
||||
</details>
|
||||
</details>
|
||||
|
||||
</ul>
|
||||
|
||||
## Developing for LNbits
|
||||
<ul>
|
||||
<li><a href="http://docs.lnbits.org/devs/development.html">Making Estension / how to use Websockets / API reference</a></li>
|
||||
<li><a href="https://t.me/lnbits">Telegram LNbits Support Group</a></li></ul>
|
||||
</ul>
|
||||
+19
-14
@@ -15,8 +15,8 @@ By default, LNbits will use SQLite as its database. You can also use PostgreSQL
|
||||
If you have problems installing LNbits using these instructions, please have a look at the [Troubleshooting](#troubleshooting) section.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
|
||||
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
|
||||
sudo apt update
|
||||
@@ -50,9 +50,10 @@ poetry run lnbits
|
||||
#### Updating the server
|
||||
|
||||
```
|
||||
cd lnbits-legend/
|
||||
cd lnbits
|
||||
# Stop LNbits with `ctrl + x`
|
||||
git pull
|
||||
# Keep your poetry install up to date, this can be done with `poetry self update`
|
||||
poetry install --only main
|
||||
# Start LNbits with `poetry run lnbits`
|
||||
```
|
||||
@@ -62,8 +63,8 @@ poetry install --only main
|
||||
> note: currently not supported while we make some architectural changes on the path to leave beta
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
# Modern debian distros usually include Nix, however you can install with:
|
||||
# 'sh <(curl -L https://nixos.org/nix/install) --daemon', or use setup here https://nixos.org/download.html#nix-verify-installation
|
||||
|
||||
@@ -82,8 +83,8 @@ LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT
|
||||
## Option 3: venv
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend/
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3.9-venv'
|
||||
python3.9 -m venv venv
|
||||
# If you have problems here, try `sudo apt install -y pkg-config libpq-dev`
|
||||
@@ -105,9 +106,9 @@ If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`
|
||||
## Option 4: Docker
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend
|
||||
docker build -t lnbits-legend .
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits .
|
||||
cp .env.example .env
|
||||
mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
|
||||
@@ -135,8 +136,8 @@ You can either run those commands, then `source ~/.bash_profile` or, if you don'
|
||||
Once installed, run the following commands.
|
||||
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
fly auth login
|
||||
[complete login process]
|
||||
fly launch
|
||||
@@ -206,6 +207,10 @@ poetry add setuptools wheel
|
||||
./venv/bin/pip install setuptools wheel
|
||||
```
|
||||
|
||||
#### Poetry
|
||||
|
||||
If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
|
||||
|
||||
### Optional: PostgreSQL database
|
||||
|
||||
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
|
||||
@@ -433,8 +438,8 @@ If you want to run LNbits on your Umbrel but want it to be reached through clear
|
||||
To install using docker you first need to build the docker image as:
|
||||
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits-legend.git
|
||||
cd lnbits-legend
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbits-legend .
|
||||
```
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
};
|
||||
outputs = { self, nixpkgs, poetry2nix }@inputs:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
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.overlay self.overlays.default ]; }));
|
||||
|
||||
+10
-11
@@ -1,16 +1,15 @@
|
||||
import hashlib
|
||||
import re
|
||||
import time
|
||||
from binascii import unhexlify
|
||||
from decimal import Decimal
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
import bitstring # type: ignore
|
||||
import bitstring
|
||||
import embit
|
||||
import secp256k1
|
||||
from bech32 import CHARSET, bech32_decode, bech32_encode
|
||||
from ecdsa import SECP256k1, VerifyingKey # type: ignore
|
||||
from ecdsa.util import sigdecode_string # type: ignore
|
||||
from ecdsa import SECP256k1, VerifyingKey
|
||||
from ecdsa.util import sigdecode_string
|
||||
|
||||
|
||||
class Route(NamedTuple):
|
||||
@@ -75,7 +74,7 @@ def decode(pr: str) -> Invoice:
|
||||
data_length = len(tagdata) / 5
|
||||
|
||||
if tag == "d":
|
||||
invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
|
||||
invoice.description = _trim_to_bytes(tagdata).decode()
|
||||
elif tag == "h" and data_length == 52:
|
||||
invoice.description_hash = _trim_to_bytes(tagdata).hex()
|
||||
elif tag == "p" and data_length == 52:
|
||||
@@ -108,7 +107,7 @@ def decode(pr: str) -> Invoice:
|
||||
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||
sig = signature[0:64]
|
||||
if invoice.payee:
|
||||
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
|
||||
key = VerifyingKey.from_string(bytes.fromhex(invoice.payee), curve=SECP256k1)
|
||||
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
|
||||
else:
|
||||
keys = VerifyingKey.from_public_key_recovery(
|
||||
@@ -131,7 +130,7 @@ def encode(options):
|
||||
if options["timestamp"]:
|
||||
addr.date = int(options["timestamp"])
|
||||
|
||||
addr.paymenthash = unhexlify(options["paymenthash"])
|
||||
addr.paymenthash = bytes.fromhex(options["paymenthash"])
|
||||
|
||||
if options["description"]:
|
||||
addr.tags.append(("d", options["description"]))
|
||||
@@ -149,8 +148,8 @@ def encode(options):
|
||||
while len(splits) >= 5:
|
||||
route.append(
|
||||
(
|
||||
unhexlify(splits[0]),
|
||||
unhexlify(splits[1]),
|
||||
bytes.fromhex(splits[0]),
|
||||
bytes.fromhex(splits[1]),
|
||||
int(splits[2]),
|
||||
int(splits[3]),
|
||||
int(splits[4]),
|
||||
@@ -235,7 +234,7 @@ def lnencode(addr, privkey):
|
||||
raise ValueError("Must include either 'd' or 'h'")
|
||||
|
||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
|
||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
|
||||
privkey = secp256k1.PrivateKey(bytes.fromhex(privkey))
|
||||
sig = privkey.ecdsa_sign_recoverable(
|
||||
bytearray([ord(c) for c in hrp]) + data.tobytes()
|
||||
)
|
||||
@@ -261,7 +260,7 @@ class LnAddr(object):
|
||||
|
||||
def __str__(self):
|
||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||
hexlify(self.pubkey.serialize()).decode("utf-8"),
|
||||
bytes.hex(self.pubkey.serialize()).decode(),
|
||||
self.amount,
|
||||
self.currency,
|
||||
", ".join([k + "=" + str(v) for k, v in self.tags]),
|
||||
|
||||
+72
-1
@@ -4,12 +4,14 @@ from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
import shortuuid
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import COCKROACH, POSTGRES, Connection
|
||||
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
|
||||
|
||||
from . import db
|
||||
from .models import BalanceCheck, Payment, User, Wallet
|
||||
from .models import BalanceCheck, Payment, TinyURL, User, Wallet
|
||||
|
||||
# accounts
|
||||
# --------
|
||||
@@ -451,6 +453,34 @@ async def update_payment_details(
|
||||
return
|
||||
|
||||
|
||||
async def update_payment_extra(
|
||||
payment_hash: str,
|
||||
extra: dict,
|
||||
outgoing: bool = False,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Only update the `extra` field for the payment.
|
||||
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
||||
"""
|
||||
|
||||
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}",
|
||||
(payment_hash,),
|
||||
)
|
||||
if not row:
|
||||
return
|
||||
db_extra = json.loads(row["extra"] if row["extra"] else "{}")
|
||||
db_extra.update(extra)
|
||||
|
||||
await (conn or db).execute(
|
||||
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
|
||||
(json.dumps(db_extra), payment_hash),
|
||||
)
|
||||
|
||||
|
||||
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
||||
@@ -592,3 +622,44 @@ async def create_admin_settings(super_user: str, new_settings: dict):
|
||||
sql = f"INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
|
||||
await db.execute(sql, (super_user, json.dumps(new_settings)))
|
||||
return await get_super_settings()
|
||||
|
||||
|
||||
# tinyurl
|
||||
# -------
|
||||
|
||||
|
||||
async def create_tinyurl(domain: str, endless: bool, wallet: str):
|
||||
tinyurl_id = shortuuid.uuid()[:8]
|
||||
await db.execute(
|
||||
f"INSERT INTO tiny_url (id, url, endless, wallet) VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
tinyurl_id,
|
||||
domain,
|
||||
endless,
|
||||
wallet,
|
||||
),
|
||||
)
|
||||
return await get_tinyurl(tinyurl_id)
|
||||
|
||||
|
||||
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
|
||||
row = await db.fetchone(
|
||||
f"SELECT * FROM tiny_url WHERE id = ?",
|
||||
(tinyurl_id,),
|
||||
)
|
||||
return TinyURL.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_tinyurl_by_url(url: str) -> List[TinyURL]:
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM tiny_url WHERE url = ?",
|
||||
(url,),
|
||||
)
|
||||
return [TinyURL.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_tinyurl(tinyurl_id: str):
|
||||
row = await db.execute(
|
||||
f"DELETE FROM tiny_url WHERE id = ?",
|
||||
(tinyurl_id,),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import OperationalError # type: ignore
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from lnbits import bolt11
|
||||
|
||||
@@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db):
|
||||
)
|
||||
).fetchall()
|
||||
if len(rows):
|
||||
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
||||
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
|
||||
for i, (
|
||||
payment_request,
|
||||
checking_id,
|
||||
@@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db):
|
||||
invoice.date + invoice.expiry
|
||||
)
|
||||
logger.info(
|
||||
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
@@ -269,3 +269,17 @@ async def m008_create_admin_settings_table(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m009_create_tinyurl_table(db):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT,
|
||||
endless BOOL NOT NULL DEFAULT false,
|
||||
wallet TEXT,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
+19
-7
@@ -4,13 +4,13 @@ import hmac
|
||||
import json
|
||||
import time
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
from fastapi import Query
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl import encode as lnurl_encode
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Extra, validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection
|
||||
from lnbits.helpers import url_for
|
||||
@@ -46,8 +46,8 @@ class Wallet(BaseModel):
|
||||
return ""
|
||||
|
||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
||||
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
||||
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
|
||||
@@ -88,7 +88,7 @@ class Payment(BaseModel):
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
expiry: Optional[float]
|
||||
extra: Optional[Dict] = {}
|
||||
extra: Dict = {}
|
||||
wallet_id: str
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
@@ -213,3 +213,15 @@ class BalanceCheck(BaseModel):
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
|
||||
|
||||
|
||||
class TinyURL(BaseModel):
|
||||
id: str
|
||||
url: str
|
||||
endless: bool
|
||||
wallet: str
|
||||
time: float
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
+5
-12
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
@@ -8,17 +7,12 @@ from urllib.parse import parse_qs, urlparse
|
||||
import httpx
|
||||
from fastapi import Depends, WebSocket
|
||||
from lnurl import LnurlErrorResponse
|
||||
from lnurl import decode as decode_lnurl # type: ignore
|
||||
from lnurl import decode as decode_lnurl
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import (
|
||||
@@ -50,7 +44,7 @@ from .crud import (
|
||||
from .models import Payment
|
||||
|
||||
try:
|
||||
from typing import TypedDict # type: ignore
|
||||
from typing import TypedDict
|
||||
except ImportError: # pragma: nocover
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -308,7 +302,7 @@ async def perform_lnurlauth(
|
||||
) -> Optional[LnurlErrorResponse]:
|
||||
cb = urlparse(callback)
|
||||
|
||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
||||
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
||||
|
||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||
|
||||
@@ -438,9 +432,8 @@ async def check_admin_settings():
|
||||
for key, value in settings.dict(exclude_none=True).items():
|
||||
logger.debug(f"{key}: {value}")
|
||||
|
||||
http = "https" if settings.lnbits_force_https else "http"
|
||||
admin_url = (
|
||||
f"{http}://{settings.host}:{settings.port}/wallet?usr={settings.super_user}"
|
||||
f"http://{settings.host}:{settings.port}/wallet?usr={settings.super_user}"
|
||||
)
|
||||
logger.success(f"✔️ Access super user account at: {admin_url}")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Dict
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||
|
||||
from . import db
|
||||
|
||||
@@ -141,15 +141,15 @@
|
||||
return {
|
||||
settings: {},
|
||||
lnbits_theme_options: [
|
||||
'classic',
|
||||
'bitcoin',
|
||||
'flamingo',
|
||||
'freedom',
|
||||
'mint',
|
||||
'autumn',
|
||||
'monochrome',
|
||||
'salvador'
|
||||
],
|
||||
'classic',
|
||||
'bitcoin',
|
||||
'flamingo',
|
||||
'freedom',
|
||||
'mint',
|
||||
'autumn',
|
||||
'monochrome',
|
||||
'salvador'
|
||||
],
|
||||
formData: {},
|
||||
formAddAdmin: '',
|
||||
formAddUser: '',
|
||||
@@ -204,11 +204,11 @@
|
||||
value: null,
|
||||
label: 'Certificate'
|
||||
},
|
||||
lnd_admin_macaroon: {
|
||||
lnd_rest_admin_macaroon: {
|
||||
value: null,
|
||||
label: 'Admin Macaroon'
|
||||
},
|
||||
lnd_invoice_macaroon: {
|
||||
lnd_rest_invoice_macaroon: {
|
||||
value: null,
|
||||
label: 'Invoice Macaroon'
|
||||
}
|
||||
|
||||
@@ -23,14 +23,55 @@
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-icon
|
||||
:name="extension.icon"
|
||||
color="grey-5"
|
||||
style="font-size: 4rem"
|
||||
></q-icon>
|
||||
{% raw %}
|
||||
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
|
||||
<small>{{ extension.shortDescription }} </small>{% endraw %}
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<q-img
|
||||
:src="extension.tile"
|
||||
spinner-color="white"
|
||||
style="max-width: 100%"
|
||||
></q-img>
|
||||
</div>
|
||||
<div class="col-9 q-pl-sm">
|
||||
{% raw %}
|
||||
<div class="text-h5 gt-sm q-mt-sm q-mb-xs">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle2 gt-sm"
|
||||
style="font-size: 11px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription }}
|
||||
</div>
|
||||
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
|
||||
{{ extension.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle2 lt-md"
|
||||
style="font-size: 9px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription }}
|
||||
</div>
|
||||
{% endraw %}
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div>
|
||||
<q-rating
|
||||
class="gt-sm"
|
||||
disable
|
||||
size="2em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
></q-rating
|
||||
><q-rating
|
||||
class="lt-md"
|
||||
size="1.5em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
></q-rating
|
||||
><q-tooltip>Ratings coming soon</q-tooltip>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-actions>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits-legend"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View project in GitHub</q-btn
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
|
||||
{{LNBITS_DENOMINATION}}
|
||||
<q-btn
|
||||
v-if="'{{user.admin}}' == 'True'"
|
||||
v-if="'{{user.super_user}}' == 'True'"
|
||||
flat
|
||||
round
|
||||
color="primary"
|
||||
@@ -36,27 +36,16 @@
|
||||
v-model="credit"
|
||||
>
|
||||
<q-input
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
label="Amount to credit account"
|
||||
filled
|
||||
label="{{LNBITS_DENOMINATION}} to credit"
|
||||
hint="Press Enter to credit account"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
mask="#.##"
|
||||
:mask="'{{LNBITS_DENOMINATION}}' != 'sats' ? '#.##' : '#'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-else
|
||||
type="number"
|
||||
label="Amount to credit account"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
autofocus
|
||||
:step="'{{LNBITS_DENOMINATION}}' != 'sats' ? '0.01' : '1'"
|
||||
@keyup.enter="updateBalance(scope.value)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
@@ -231,7 +220,7 @@
|
||||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="props.row.bolt11"
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
@@ -325,7 +314,7 @@
|
||||
</p>
|
||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||
<qrcode
|
||||
value="{{wallet.lnurlwithdraw_full}}"
|
||||
value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
:options="{width:240}"
|
||||
></qrcode>
|
||||
</a>
|
||||
@@ -524,7 +513,7 @@
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="receive.paymentReq"
|
||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
|
||||
@@ -17,7 +17,7 @@ from ..crud import delete_admin_settings, get_admin_settings, update_admin_setti
|
||||
|
||||
@core_app.get("/admin/api/v1/settings/")
|
||||
async def api_get_settings(
|
||||
user: User = Depends(check_admin), # type: ignore
|
||||
user: User = Depends(check_admin),
|
||||
) -> Optional[AdminSettings]:
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
return admin_settings
|
||||
|
||||
+93
-17
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
@@ -13,6 +12,7 @@ import async_timeout
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import (
|
||||
Body,
|
||||
Depends,
|
||||
Header,
|
||||
Query,
|
||||
@@ -22,12 +22,11 @@ from fastapi import (
|
||||
WebSocketDisconnect,
|
||||
)
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Body
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from starlette.responses import StreamingResponse
|
||||
from starlette.responses import RedirectResponse, StreamingResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
from lnbits.core.models import Payment, Wallet
|
||||
@@ -38,7 +37,7 @@ from lnbits.decorators import (
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
@@ -48,14 +47,15 @@ from lnbits.utils.exchange_rates import (
|
||||
|
||||
from .. import core_app, db
|
||||
from ..crud import (
|
||||
create_payment,
|
||||
create_tinyurl,
|
||||
delete_tinyurl,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_tinyurl,
|
||||
get_tinyurl_by_url,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_payment_status,
|
||||
update_wallet,
|
||||
)
|
||||
from ..services import (
|
||||
@@ -71,6 +71,11 @@ from ..services import (
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
|
||||
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
|
||||
async def health():
|
||||
return
|
||||
|
||||
|
||||
@core_app.get("/api/v1/wallet")
|
||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
if wallet.wallet_type == 0:
|
||||
@@ -140,16 +145,14 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
if data.description_hash or data.unhashed_description:
|
||||
try:
|
||||
description_hash = (
|
||||
binascii.unhexlify(data.description_hash)
|
||||
if data.description_hash
|
||||
else b""
|
||||
bytes.fromhex(data.description_hash) if data.description_hash else b""
|
||||
)
|
||||
unhashed_description = (
|
||||
binascii.unhexlify(data.unhashed_description)
|
||||
bytes.fromhex(data.unhashed_description)
|
||||
if data.unhashed_description
|
||||
else b""
|
||||
)
|
||||
except binascii.Error:
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||
@@ -214,7 +217,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
lnurl_response = resp["reason"]
|
||||
else:
|
||||
lnurl_response = True
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
except (httpx.ConnectError, httpx.RequestError) as ex:
|
||||
logger.error(ex)
|
||||
lnurl_response = False
|
||||
|
||||
return {
|
||||
@@ -251,7 +255,7 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||
)
|
||||
async def api_payments_create(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
invoiceData: CreateInvoiceData = Body(...), # type: ignore
|
||||
invoiceData: CreateInvoiceData = Body(...),
|
||||
):
|
||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||
if not invoiceData.bolt11:
|
||||
@@ -387,7 +391,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
||||
jdata = json.dumps(dict(data.dict(), pending=False))
|
||||
|
||||
yield dict(data=jdata, event=typ)
|
||||
except asyncio.CancelledError as e:
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"removing listener for wallet {uid}")
|
||||
api_invoice_listeners.pop(uid)
|
||||
task.cancel()
|
||||
@@ -536,7 +540,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
||||
|
||||
params.update(
|
||||
description_hash=hashlib.sha256(
|
||||
data["metadata"].encode("utf-8")
|
||||
data["metadata"].encode()
|
||||
).hexdigest()
|
||||
)
|
||||
metadata = json.loads(data["metadata"])
|
||||
@@ -658,7 +662,7 @@ async def img(request: Request, data):
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)])
|
||||
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||
async def api_auditor():
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
@@ -706,3 +710,75 @@ async def websocket_update_get(item_id: str, data: str):
|
||||
return {"sent": True, "data": data}
|
||||
except:
|
||||
return {"sent": False, "data": data}
|
||||
|
||||
|
||||
############################TINYURL##################################
|
||||
|
||||
|
||||
@core_app.post("/api/v1/tinyurl")
|
||||
async def api_create_tinyurl(
|
||||
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
tinyurls = await get_tinyurl_by_url(url)
|
||||
try:
|
||||
for tinyurl in tinyurls:
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
return tinyurl
|
||||
return await create_tinyurl(url, endless, wallet.wallet.inkey)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/tinyurl/{tinyurl_id}")
|
||||
async def api_get_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
return tinyurl
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
|
||||
)
|
||||
|
||||
|
||||
@core_app.delete("/api/v1/tinyurl/{tinyurl_id}")
|
||||
async def api_delete_tinyurl(
|
||||
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
if tinyurl.wallet == wallet.wallet.inkey:
|
||||
await delete_tinyurl(tinyurl_id)
|
||||
return {"deleted": True}
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
|
||||
)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/t/{tinyurl_id}")
|
||||
async def api_tinyurl(tinyurl_id: str):
|
||||
try:
|
||||
tinyurl = await get_tinyurl(tinyurl_id)
|
||||
if tinyurl:
|
||||
response = RedirectResponse(url=tinyurl.url)
|
||||
return response
|
||||
else:
|
||||
return
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
|
||||
)
|
||||
|
||||
@@ -2,9 +2,8 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi import Depends, Query, Request, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Depends, Query
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from loguru import logger
|
||||
@@ -38,20 +37,29 @@ async def favicon():
|
||||
|
||||
|
||||
@core_html_routes.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request, lightning: str = None):
|
||||
async def home(request: Request, lightning: str = ""):
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/index.html", {"request": request, "lnurl": lightning}
|
||||
)
|
||||
|
||||
|
||||
@core_html_routes.get("/robots.txt", response_class=HTMLResponse)
|
||||
async def robots():
|
||||
data = """
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
"""
|
||||
return HTMLResponse(content=data, media_type="text/plain")
|
||||
|
||||
|
||||
@core_html_routes.get(
|
||||
"/extensions", name="core.extensions", response_class=HTMLResponse
|
||||
)
|
||||
async def extensions(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists), # type: ignore
|
||||
enable: str = Query(None), # type: ignore
|
||||
disable: str = Query(None), # type: ignore
|
||||
user: User = Depends(check_user_exists),
|
||||
enable: str = Query(None),
|
||||
disable: str = Query(None),
|
||||
):
|
||||
extension_to_enable = enable
|
||||
extension_to_disable = disable
|
||||
@@ -103,10 +111,10 @@ nothing: create everything<br>
|
||||
""",
|
||||
)
|
||||
async def wallet(
|
||||
request: Request = Query(None), # type: ignore
|
||||
nme: Optional[str] = Query(None), # type: ignore
|
||||
usr: Optional[UUID4] = Query(None), # type: ignore
|
||||
wal: Optional[UUID4] = Query(None), # type: ignore
|
||||
request: Request = Query(None),
|
||||
nme: Optional[str] = Query(None),
|
||||
usr: Optional[UUID4] = Query(None),
|
||||
wal: Optional[UUID4] = Query(None),
|
||||
):
|
||||
user_id = usr.hex if usr else None
|
||||
wallet_id = wal.hex if wal else None
|
||||
@@ -124,12 +132,17 @@ async def wallet(
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and user_id not in settings.lnbits_allowed_users
|
||||
and user_id not in settings.lnbits_admin_users
|
||||
and user_id != settings.super_user
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "User not authorized."}
|
||||
)
|
||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||
user.admin = True
|
||||
if user_id == settings.super_user:
|
||||
user.super_user = True
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name: # type: ignore
|
||||
wallet = user.wallets[0] # type: ignore
|
||||
@@ -214,7 +227,7 @@ async def lnurl_full_withdraw_callback(request: Request):
|
||||
|
||||
|
||||
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
|
||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
|
||||
async def deletewallet(wal: str = Query(...), usr: str = Query(...)):
|
||||
user = await get_user(usr)
|
||||
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
|
||||
|
||||
@@ -310,7 +323,7 @@ async def manifest(usr: str):
|
||||
|
||||
|
||||
@core_html_routes.get("/admin", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_admin)): # type: ignore
|
||||
async def index(request: Request, user: User = Depends(check_admin)):
|
||||
WALLET = get_wallet_class()
|
||||
_, balance = await WALLET.status()
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
|
||||
@@ -17,7 +16,7 @@ from ..tasks import api_invoice_listeners
|
||||
|
||||
@core_app.get("/.well-known/lnurlp/{username}")
|
||||
async def lnaddress(username: str, request: Request):
|
||||
from lnbits.extensions.lnaddress.lnurl import lnurl_response
|
||||
from lnbits.extensions.lnaddress.lnurl import lnurl_response # type: ignore
|
||||
|
||||
domain = urlparse(str(request.url)).netloc
|
||||
return await lnurl_response(username, domain, request)
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ from typing import Optional
|
||||
from loguru import logger
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy_aio.base import AsyncConnection
|
||||
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
|
||||
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -129,7 +129,7 @@ class Database(Compat):
|
||||
else:
|
||||
self.type = POSTGRES
|
||||
|
||||
import psycopg2 # type: ignore
|
||||
import psycopg2
|
||||
|
||||
def _parse_timestamp(value, _):
|
||||
if value is None:
|
||||
|
||||
+8
-11
@@ -1,11 +1,8 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Union
|
||||
|
||||
from cerberus import Validator # type: ignore
|
||||
from fastapi import status
|
||||
from fastapi import Security, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.params import Security
|
||||
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
|
||||
from fastapi.security.base import SecurityBase
|
||||
from pydantic.types import UUID4
|
||||
@@ -118,8 +115,8 @@ api_key_query = APIKeyQuery(
|
||||
|
||||
async def get_key_type(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
) -> WalletTypeInfo:
|
||||
# 0: admin
|
||||
# 1: invoice
|
||||
@@ -174,8 +171,8 @@ async def get_key_type(
|
||||
|
||||
async def require_admin_key(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
):
|
||||
|
||||
token = api_key_header or api_key_query
|
||||
@@ -200,8 +197,8 @@ async def require_admin_key(
|
||||
|
||||
async def require_invoice_key(
|
||||
r: Request,
|
||||
api_key_header: str = Security(api_key_header), # type: ignore
|
||||
api_key_query: str = Security(api_key_query), # type: ignore
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query),
|
||||
):
|
||||
|
||||
token = api_key_header or api_key_query
|
||||
@@ -236,8 +233,8 @@ async def check_user_exists(usr: UUID4) -> User:
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and g().user.id not in settings.lnbits_allowed_users
|
||||
and g().user.id != settings.super_user
|
||||
and g().user.id not in settings.lnbits_admin_users
|
||||
and g().user.id != settings.super_user
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Bleskomat",
|
||||
"short_description": "Connect a Bleskomat ATM to an lnbits",
|
||||
"icon": "money",
|
||||
"tile": "/bleskomat/static/image/bleskomat.png",
|
||||
"contributors": ["chill117"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Callable, Dict, Union
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -12,7 +13,9 @@ fiat_currencies = json.load(
|
||||
)
|
||||
)
|
||||
|
||||
exchange_rate_providers = {
|
||||
exchange_rate_providers: dict[
|
||||
str, dict[str, Union[str, Callable[[dict, dict], str]]]
|
||||
] = {
|
||||
"bitfinex": {
|
||||
"name": "Bitfinex",
|
||||
"domain": "bitfinex.com",
|
||||
@@ -65,17 +68,19 @@ async def fetch_fiat_exchange_rate(currency: str, provider: str):
|
||||
"to": currency.lower(),
|
||||
}
|
||||
|
||||
url = exchange_rate_providers[provider]["api_url"]
|
||||
if url:
|
||||
api_url_or_none = exchange_rate_providers[provider]["api_url"]
|
||||
if api_url_or_none is not None:
|
||||
api_url = str(api_url_or_none)
|
||||
for key in replacements.keys():
|
||||
url = url.replace("{" + key + "}", replacements[key])
|
||||
api_url = api_url.replace("{" + key + "}", replacements[key])
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(url)
|
||||
r = await client.get(api_url)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
else:
|
||||
data = {}
|
||||
|
||||
getter = exchange_rate_providers[provider]["getter"]
|
||||
rate = float(getter(data, replacements))
|
||||
print(getter)
|
||||
if callable(getter):
|
||||
rate = float(getter(data, replacements))
|
||||
return rate
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import urllib
|
||||
from binascii import unhexlify
|
||||
from http import HTTPStatus
|
||||
from typing import Dict
|
||||
from urllib import parse
|
||||
|
||||
from starlette.requests import Request
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
def generate_bleskomat_lnurl_hash(secret: str):
|
||||
@@ -19,11 +18,11 @@ def generate_bleskomat_lnurl_signature(
|
||||
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
|
||||
):
|
||||
if api_key_encoding == "hex":
|
||||
key = unhexlify(api_key_secret)
|
||||
key = bytes.fromhex(api_key_secret)
|
||||
elif api_key_encoding == "base64":
|
||||
key = base64.b64decode(api_key_secret)
|
||||
else:
|
||||
key = bytes(f"{api_key_secret}")
|
||||
key = bytes.fromhex(api_key_secret)
|
||||
return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
@@ -58,8 +57,8 @@ class LnurlValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def prepare_lnurl_params(tag: str, query: Dict[str, str]):
|
||||
params = {}
|
||||
def prepare_lnurl_params(tag: str, query: dict) -> dict:
|
||||
params: dict = {}
|
||||
if not is_supported_lnurl_subprotocol(tag):
|
||||
raise LnurlValidationError(f'Unsupported subprotocol: "{tag}"')
|
||||
if tag == "withdrawRequest":
|
||||
@@ -86,15 +85,15 @@ def query_to_signing_payload(query: Dict[str, str]) -> str:
|
||||
payload = []
|
||||
for key in sorted_keys:
|
||||
if not key == "signature":
|
||||
encoded_key = urllib.parse.quote(key, safe=encode_uri_component_safe_chars)
|
||||
encoded_value = urllib.parse.quote(
|
||||
encoded_key = parse.quote(key, safe=encode_uri_component_safe_chars)
|
||||
encoded_value = parse.quote(
|
||||
query[key], safe=encode_uri_component_safe_chars
|
||||
)
|
||||
payload.append(f"{encoded_key}={encoded_value}")
|
||||
return "&".join(payload)
|
||||
|
||||
|
||||
unshorten_rules = {
|
||||
unshorten_rules: dict[str, dict] = {
|
||||
"query": {"n": "nonce", "s": "signature", "t": "tag"},
|
||||
"tags": {
|
||||
"c": "channelRequest",
|
||||
@@ -115,7 +114,7 @@ unshorten_rules = {
|
||||
}
|
||||
|
||||
|
||||
def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
def unshorten_lnurl_query(query: dict) -> Dict[str, str]:
|
||||
new_query = {}
|
||||
rules = unshorten_rules
|
||||
if "tag" in query:
|
||||
@@ -132,9 +131,9 @@ def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
if not tag in rules["params"]:
|
||||
raise LnurlValidationError(f'Unknown tag: "{tag}"')
|
||||
for key in query:
|
||||
if key in rules["params"][tag]:
|
||||
if key in rules["params"][str(tag)]:
|
||||
short_param_key = key
|
||||
long_param_key = rules["params"][tag][short_param_key]
|
||||
long_param_key = rules["params"][str(tag)][short_param_key]
|
||||
if short_param_key in query:
|
||||
new_query[long_param_key] = query[short_param_key]
|
||||
else:
|
||||
@@ -147,7 +146,7 @@ def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
if short_key in query:
|
||||
new_query[long_key] = query[short_key]
|
||||
else:
|
||||
new_query[long_key] = query[long_key]
|
||||
new_query[long_key] = query[str(long_key)]
|
||||
else:
|
||||
# Keep unknown key/value pairs unchanged:
|
||||
new_query[key] = query[key]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import math
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
|
||||
from loguru import logger
|
||||
@@ -28,7 +27,7 @@ from .helpers import (
|
||||
@bleskomat_ext.get("/u", name="bleskomat.api_bleskomat_lnurl")
|
||||
async def api_bleskomat_lnurl(req: Request):
|
||||
try:
|
||||
query = req.query_params
|
||||
query = dict(req.query_params)
|
||||
|
||||
# Unshorten query if "s" is used instead of "signature".
|
||||
if "s" in query:
|
||||
@@ -89,11 +88,15 @@ async def api_bleskomat_lnurl(req: Request):
|
||||
# Convert to msats:
|
||||
params[key] = int(amount_sats_less_fee * 1e3)
|
||||
except LnurlValidationError as e:
|
||||
raise LnurlHttpError(e.message, HTTPStatus.BAD_REQUEST)
|
||||
raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
|
||||
# Create a new LNURL using the query parameters provided in the signed URL.
|
||||
params = json.JSONEncoder().encode(params)
|
||||
json_params = json.JSONEncoder().encode(params)
|
||||
lnurl = await create_bleskomat_lnurl(
|
||||
bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1
|
||||
bleskomat=bleskomat,
|
||||
secret=secret,
|
||||
tag=tag,
|
||||
params=json_params,
|
||||
uses=1,
|
||||
)
|
||||
|
||||
# Reply with LNURL response object.
|
||||
|
||||
@@ -2,10 +2,9 @@ import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.params import Query
|
||||
from fastapi import Query, Request
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, validator
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.services import PaymentFailure, pay_invoice
|
||||
@@ -80,7 +79,7 @@ class BleskomatLnurl(BaseModel):
|
||||
response["k1"] = secret
|
||||
return response
|
||||
|
||||
def validate_action(self, query: Dict[str, str]) -> None:
|
||||
def validate_action(self, query) -> None:
|
||||
tag = self.tag
|
||||
params = json.loads(self.params)
|
||||
# Perform tag-specific checks.
|
||||
@@ -109,7 +108,7 @@ class BleskomatLnurl(BaseModel):
|
||||
else:
|
||||
raise LnurlValidationError(f'Unknown subprotocol: "{tag}"')
|
||||
|
||||
async def execute_action(self, query: Dict[str, str]):
|
||||
async def execute_action(self, query):
|
||||
self.validate_action(query)
|
||||
used = False
|
||||
async with db.connect() as conn:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -9,11 +9,13 @@
|
||||
<p>
|
||||
This extension allows you to connect a Bleskomat ATM to an lnbits
|
||||
wallet. It will work with both the
|
||||
<a href="https://github.com/samotari/bleskomat"
|
||||
<a class="text-secondary" href="https://github.com/samotari/bleskomat"
|
||||
>open-source DIY Bleskomat ATM project</a
|
||||
>
|
||||
as well as the
|
||||
<a href="https://www.bleskomat.com/">commercial Bleskomat ATM</a>.
|
||||
<a class="text-secondary" href="https://www.bleskomat.com/"
|
||||
>commercial Bleskomat ATM</a
|
||||
>.
|
||||
</p>
|
||||
<h5 class="text-subtitle1 q-my-none">Connect Your Bleskomat ATM</h5>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.extensions.bleskomat.models import CreateBleskomat
|
||||
|
||||
from . import bleskomat_ext
|
||||
from .crud import (
|
||||
@@ -17,6 +16,7 @@ from .crud import (
|
||||
update_bleskomat,
|
||||
)
|
||||
from .exchange_rates import fetch_fiat_exchange_rate
|
||||
from .models import CreateBleskomat
|
||||
|
||||
|
||||
@bleskomat_ext.get("/api/v1/bleskomats")
|
||||
@@ -27,7 +27,8 @@ async def api_bleskomats(
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
user = await get_user(wallet.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return [bleskomat.dict() for bleskomat in await get_bleskomats(wallet_ids)]
|
||||
|
||||
@@ -54,9 +55,9 @@ async def api_bleskomat_create_or_update(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
bleskomat_id=None,
|
||||
):
|
||||
fiat_currency = data.fiat_currency
|
||||
exchange_rate_provider = data.exchange_rate_provider
|
||||
try:
|
||||
fiat_currency = data.fiat_currency
|
||||
exchange_rate_provider = data.exchange_rate_provider
|
||||
await fetch_fiat_exchange_rate(
|
||||
currency=fiat_currency, provider=exchange_rate_provider
|
||||
)
|
||||
@@ -79,6 +80,7 @@ async def api_bleskomat_create_or_update(
|
||||
else:
|
||||
bleskomat = await create_bleskomat(wallet_id=wallet.wallet.id, data=data)
|
||||
|
||||
assert bleskomat
|
||||
return bleskomat.dict()
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This extension allows you to link your Bolt Card (or other compatible NXP NTAG device) with a LNbits instance and use it in a more secure way than a static LNURLw. A technology called [Secure Unique NFC](https://mishka-scan.com/blog/secure-unique-nfc) is utilized in this workflow.
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
||||
<a class="text-secondary" href="https://www.youtube.com/watch?v=wJ7QLFTRjK0">Tutorial</a>
|
||||
|
||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||
|
||||
@@ -55,6 +55,8 @@ Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys
|
||||
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||
|
||||
If you somehow find yourself in some non-standard state (for instance only k3 and k4 remains filled after previous unsuccessful reset), then you need edit the key fields manually (for instance leave k0-k2 to zeroes and provide the right k3 and k4).
|
||||
|
||||
## Setting the card - computer (hard way)
|
||||
|
||||
Follow the guide.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Bolt Cards",
|
||||
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
||||
"icon": "payment",
|
||||
"tile": "/boltcards/static/image/boltcard.png",
|
||||
"contributors": ["iwarpbtc", "arcbtc", "leesalminen"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import secrets
|
||||
from datetime import date, datetime
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
@@ -66,9 +66,9 @@ async def update_card(card_id: str, **kwargs) -> Optional[Card]:
|
||||
return Card(**row) if row else None
|
||||
|
||||
|
||||
async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
async def get_cards(wallet_ids: List[str]) -> List[Card]:
|
||||
if len(wallet_ids) == 0:
|
||||
return []
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
@@ -124,14 +124,13 @@ async def get_card_by_otp(otp: str) -> Optional[Card]:
|
||||
|
||||
async def delete_card(card_id: str) -> None:
|
||||
# Delete cards
|
||||
card = await get_card(card_id)
|
||||
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||
# Delete hits
|
||||
hits = await get_hits([card_id])
|
||||
for hit in hits:
|
||||
await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
|
||||
# Delete refunds
|
||||
refunds = await get_refunds([hit])
|
||||
refunds = await get_refunds([hit.id])
|
||||
for refund in refunds:
|
||||
await db.execute(
|
||||
"DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
|
||||
@@ -146,7 +145,7 @@ async def update_card_counter(counter: int, id: str):
|
||||
|
||||
|
||||
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
|
||||
row = await db.execute(
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
||||
(enable, id),
|
||||
)
|
||||
@@ -161,7 +160,7 @@ async def update_card_otp(otp: str, id: str):
|
||||
|
||||
|
||||
async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||
row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
|
||||
row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id,))
|
||||
if not row:
|
||||
return None
|
||||
|
||||
@@ -170,7 +169,7 @@ async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||
return Hit.parse_obj(hit)
|
||||
|
||||
|
||||
async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
|
||||
async def get_hits(cards_ids: List[str]) -> List[Hit]:
|
||||
if len(cards_ids) == 0:
|
||||
return []
|
||||
|
||||
@@ -182,7 +181,7 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
|
||||
return [Hit(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_hits_today(card_id: str) -> Optional[Hit]:
|
||||
async def get_hits_today(card_id: str) -> List[Hit]:
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltcards.hits WHERE card_id = ?",
|
||||
(card_id,),
|
||||
@@ -259,7 +258,7 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
||||
|
||||
async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||
row = await db.fetchone(
|
||||
f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id)
|
||||
f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id,)
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
@@ -267,7 +266,7 @@ async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||
return Refund.parse_obj(refund)
|
||||
|
||||
|
||||
async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
|
||||
async def get_refunds(hits_ids: List[str]) -> List[Refund]:
|
||||
if len(hits_ids) == 0:
|
||||
return []
|
||||
|
||||
|
||||
@@ -3,13 +3,9 @@ import secrets
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends, Query
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from fastapi import HTTPException, Query, Request
|
||||
from lnurl import encode as lnurl_encode
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
@@ -28,14 +24,13 @@ from .crud import (
|
||||
update_card_counter,
|
||||
update_card_otp,
|
||||
)
|
||||
from .models import CreateCardData
|
||||
from .nxp424 import decryptSUN, getSunMAC
|
||||
|
||||
###############LNURLWITHDRAW#################
|
||||
|
||||
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
|
||||
@boltcards_ext.get("/api/v1/scan/{external_id}")
|
||||
async def api_scan(p, c, request: Request, external_id: str = None):
|
||||
async def api_scan(p, c, request: Request, external_id: str = Query(None)):
|
||||
# some wallets send everything as lower case, no bueno
|
||||
p = p.upper()
|
||||
c = c.upper()
|
||||
@@ -63,6 +58,7 @@ async def api_scan(p, c, request: Request, external_id: str = None):
|
||||
await update_card_counter(ctr_int, card.id)
|
||||
|
||||
# gathering some info for hit record
|
||||
assert request.client
|
||||
ip = request.client.host
|
||||
if "x-real-ip" in request.headers:
|
||||
ip = request.headers["x-real-ip"]
|
||||
@@ -95,7 +91,6 @@ async def api_scan(p, c, request: Request, external_id: str = None):
|
||||
name="boltcards.lnurl_callback",
|
||||
)
|
||||
async def lnurl_callback(
|
||||
request: Request,
|
||||
pr: str = Query(None),
|
||||
k1: str = Query(None),
|
||||
):
|
||||
@@ -120,7 +115,9 @@ async def lnurl_callback(
|
||||
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
||||
|
||||
card = await get_card(hit.card_id)
|
||||
assert card
|
||||
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
||||
assert hit
|
||||
try:
|
||||
await pay_invoice(
|
||||
wallet_id=card.wallet,
|
||||
@@ -155,7 +152,7 @@ async def api_auth(a, request: Request):
|
||||
|
||||
response = {
|
||||
"card_name": card.card_name,
|
||||
"id": 1,
|
||||
"id": str(1),
|
||||
"k0": card.k0,
|
||||
"k1": card.k1,
|
||||
"k2": card.k2,
|
||||
@@ -163,7 +160,7 @@ async def api_auth(a, request: Request):
|
||||
"k4": card.k2,
|
||||
"lnurlw_base": "lnurlw://" + lnurlw_base,
|
||||
"protocol_name": "new_bolt_card_response",
|
||||
"protocol_version": 1,
|
||||
"protocol_version": str(1),
|
||||
}
|
||||
|
||||
return response
|
||||
@@ -179,7 +176,9 @@ async def api_auth(a, request: Request):
|
||||
)
|
||||
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
|
||||
hit = await get_hit(hit_id)
|
||||
assert hit
|
||||
card = await get_card(hit.card_id)
|
||||
assert card
|
||||
if not hit:
|
||||
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||
if not card.enable:
|
||||
@@ -199,21 +198,21 @@ async def lnurlp_response(req: Request, hit_id: str = Query(None)):
|
||||
response_class=HTMLResponse,
|
||||
name="boltcards.lnurlp_callback",
|
||||
)
|
||||
async def lnurlp_callback(
|
||||
req: Request, hit_id: str = Query(None), amount: str = Query(None)
|
||||
):
|
||||
async def lnurlp_callback(hit_id: str = Query(None), amount: str = Query(None)):
|
||||
hit = await get_hit(hit_id)
|
||||
assert hit
|
||||
card = await get_card(hit.card_id)
|
||||
assert card
|
||||
if not hit:
|
||||
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
_, payment_request = await create_invoice(
|
||||
wallet_id=card.wallet,
|
||||
amount=int(amount) / 1000,
|
||||
amount=int(int(amount) / 1000),
|
||||
memo=f"Refund {hit_id}",
|
||||
unhashed_description=LnurlPayMetadata(
|
||||
json.dumps([["text/plain", "Refund"]])
|
||||
).encode("utf-8"),
|
||||
).encode(),
|
||||
extra={"refund": hit_id},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import json
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Query
|
||||
from fastapi import Query, Request
|
||||
from lnurl import Lnurl
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from lnurl import encode as lnurl_encode
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from pydantic import BaseModel
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
ZERO_KEY = "00000000000000000000000000000000"
|
||||
|
||||
@@ -32,6 +29,7 @@ class Card(BaseModel):
|
||||
otp: str
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Card":
|
||||
return cls(**dict(row))
|
||||
|
||||
@@ -40,7 +38,7 @@ class Card(BaseModel):
|
||||
return lnurl_encode(url)
|
||||
|
||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||
return LnurlPayMetadata(json.dumps([["text/plain", self.card_name]]))
|
||||
|
||||
|
||||
class CreateCardData(BaseModel):
|
||||
@@ -69,6 +67,7 @@ class Hit(BaseModel):
|
||||
amount: int
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Hit":
|
||||
return cls(**dict(row))
|
||||
|
||||
@@ -79,5 +78,6 @@ class Refund(BaseModel):
|
||||
refund_amount: int
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Refund":
|
||||
return cls(**dict(row))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,8 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
@@ -21,22 +19,23 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
|
||||
if not payment.extra.get("refund"):
|
||||
return
|
||||
|
||||
if payment.extra.get("wh_status"):
|
||||
# this webhook has already been sent
|
||||
return
|
||||
hit = await get_hit(payment.extra.get("refund"))
|
||||
|
||||
hit = await get_hit(str(payment.extra.get("refund")))
|
||||
|
||||
if hit:
|
||||
refund = await create_refund(
|
||||
hit_id=hit.id, refund_amount=(payment.amount / 1000)
|
||||
)
|
||||
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||
await mark_webhook_sent(payment, 1)
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
|
||||
payment.extra["wh_status"] = status
|
||||
|
||||
await core_db.execute(
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
Manage your Bolt Cards self custodian way<br />
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards"
|
||||
>More details</a
|
||||
>
|
||||
|
||||
@@ -375,6 +375,7 @@
|
||||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||
(QR for <strong>create</strong> the card in
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||
target="_blank"
|
||||
style="color: inherit"
|
||||
@@ -395,6 +396,7 @@
|
||||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||
(QR for <strong>wipe</strong> the card in
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||
target="_blank"
|
||||
style="color: inherit"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import secrets
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi.params import Depends, Query
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from fastapi import Depends, HTTPException, Query
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
@@ -15,13 +11,11 @@ from .crud import (
|
||||
delete_card,
|
||||
enable_disable_card,
|
||||
get_card,
|
||||
get_card_by_otp,
|
||||
get_card_by_uid,
|
||||
get_cards,
|
||||
get_hits,
|
||||
get_refunds,
|
||||
update_card,
|
||||
update_card_otp,
|
||||
)
|
||||
from .models import CreateCardData
|
||||
|
||||
@@ -33,7 +27,8 @@ async def api_cards(
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return [card.dict() for card in await get_cards(wallet_ids)]
|
||||
|
||||
@@ -41,9 +36,8 @@ async def api_cards(
|
||||
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
|
||||
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
|
||||
async def api_card_create_or_update(
|
||||
# req: Request,
|
||||
data: CreateCardData,
|
||||
card_id: str = None,
|
||||
card_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
try:
|
||||
@@ -95,6 +89,7 @@ async def api_card_create_or_update(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
card = await create_card(wallet_id=wallet.wallet.id, data=data)
|
||||
assert card
|
||||
return card.dict()
|
||||
|
||||
|
||||
@@ -110,6 +105,7 @@ async def enable_card(
|
||||
if card.wallet != wallet.wallet.id:
|
||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||
card = await enable_disable_card(enable=enable, id=card_id)
|
||||
assert card
|
||||
return card.dict()
|
||||
|
||||
|
||||
@@ -136,7 +132,8 @@ async def api_hits(
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
cards = await get_cards(wallet_ids)
|
||||
cards_ids = []
|
||||
@@ -147,13 +144,14 @@ async def api_hits(
|
||||
|
||||
|
||||
@boltcards_ext.get("/api/v1/refunds")
|
||||
async def api_hits(
|
||||
async def api_refunds(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
cards = await get_cards(wallet_ids)
|
||||
cards_ids = []
|
||||
|
||||
@@ -5,11 +5,12 @@ move **IN** and **OUT** of the **lightning network** and remain in control of yo
|
||||
* [Documentation](https://docs.boltz.exchange/en/latest/)
|
||||
* [Discord](https://discord.gg/d6EK85KK)
|
||||
* [Twitter](https://twitter.com/Boltzhq)
|
||||
* [FAQ](https://www.notion.so/Frequently-Asked-Questions-585328ae43944e2eba351050790d5eec) very cool!
|
||||
|
||||
# usage
|
||||
This extension lets you create swaps, reverse swaps and in the case of failure refund your onchain funds.
|
||||
|
||||
## create normal swap
|
||||
## create normal swap (Onchain -> Lightning)
|
||||
1. click on "Swap (IN)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to do your refund to if the swap fails after you already commited onchain funds.
|
||||
---
|
||||

|
||||
@@ -22,14 +23,14 @@ This extension lets you create swaps, reverse swaps and in the case of failure r
|
||||
|
||||
if anything goes wrong when boltz is trying to pay your invoice, the swap will fail and you will need to refund your onchain funds after the timeout block height hit. (if boltz can pay the invoice, it wont be able to redeem your onchain funds either).
|
||||
|
||||
## create reverse swap
|
||||
## create reverse swap (Lightning -> Onchain)
|
||||
1. click on "Swap (OUT)" button to open following dialog, select a wallet, choose a proper amount in the min-max range and choose a onchain address to receive your funds to. Instant settlement: means that LNbits will create the onchain claim transaction if it sees the boltz lockup transaction in the mempool, but it is not confirmed yet. it is advised to leave this checked because it is faster and the longer is takes to settle, the higher the chances are that the lightning invoice expires and the swap fails.
|
||||
---
|
||||

|
||||
---
|
||||
if this swap fails, boltz is doing the onchain refunding, because they have to commit onchain funds.
|
||||
|
||||
# refund locked onchain funds from a normal swap
|
||||
# refund locked onchain funds from a normal swap (Onchain -> Lightning)
|
||||
if for some reason the normal swap fails and you already paid onchain, you can easily refund your btc.
|
||||
this can happen if boltz is not able to pay your lightning invoice after you locked up your funds.
|
||||
in case that happens, there is a info icon in the Swap (In) List which opens following dialog.
|
||||
@@ -37,4 +38,5 @@ in case that happens, there is a info icon in the Swap (In) List which opens fol
|
||||

|
||||
----
|
||||
if the timeout block height is exceeded you can either press refund and lnbits will do the refunding to the address you specified when creating the swap. Or download the refundfile so you can manually refund your onchain directly on the boltz.exchange website.
|
||||
if you think there is something wrong and/or you are unsure, you can ask for help either in LNbits telegram or in Boltz [Discord](https://discord.gg/d6EK85KK)
|
||||
if you think there is something wrong and/or you are unsure, you can ask for help either in LNbits telegram or in Boltz [Discord](https://discord.gg/d6EK85KK).
|
||||
In a recent update we made *automated check*, every 15 minutes, to check if LNbits can refund your failed swap.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
@@ -15,6 +16,14 @@ def boltz_renderer():
|
||||
return template_renderer(["lnbits/extensions/boltz/templates"])
|
||||
|
||||
|
||||
boltz_static_files = [
|
||||
{
|
||||
"path": "/boltz/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/boltz/static"),
|
||||
"name": "boltz_static",
|
||||
}
|
||||
]
|
||||
|
||||
from .tasks import check_for_pending_swaps, wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import sha256
|
||||
from typing import Awaitable, Union
|
||||
|
||||
import httpx
|
||||
from embit import ec, script
|
||||
from embit.networks import NETWORKS
|
||||
from embit.transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .crud import update_swap_status
|
||||
from .mempool import (
|
||||
get_fee_estimation,
|
||||
get_mempool_blockheight,
|
||||
get_mempool_fees,
|
||||
get_mempool_tx,
|
||||
get_mempool_tx_from_txs,
|
||||
send_onchain_tx,
|
||||
wait_for_websocket_message,
|
||||
)
|
||||
from .models import (
|
||||
CreateReverseSubmarineSwap,
|
||||
CreateSubmarineSwap,
|
||||
ReverseSubmarineSwap,
|
||||
SubmarineSwap,
|
||||
SwapStatus,
|
||||
)
|
||||
from .utils import check_balance, get_timestamp, req_wrap
|
||||
|
||||
net = NETWORKS[settings.boltz_network]
|
||||
|
||||
|
||||
async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
|
||||
if not check_boltz_limits(data.amount):
|
||||
msg = f"Boltz - swap not in boltz limits"
|
||||
logger.warning(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
swap_id = urlsafe_short_hash()
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=data.wallet,
|
||||
amount=data.amount,
|
||||
memo=f"swap of {data.amount} sats on boltz.exchange",
|
||||
extra={"tag": "boltz", "swap_id": swap_id},
|
||||
)
|
||||
except Exception as exc:
|
||||
msg = f"Boltz - create_invoice failed {str(exc)}"
|
||||
logger.error(msg)
|
||||
raise
|
||||
|
||||
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||
refund_pubkey_hex = hexlify(refund_privkey.sec()).decode("UTF-8")
|
||||
|
||||
res = req_wrap(
|
||||
"post",
|
||||
f"{settings.boltz_url}/createswap",
|
||||
json={
|
||||
"type": "submarine",
|
||||
"pairId": "BTC/BTC",
|
||||
"orderSide": "sell",
|
||||
"refundPublicKey": refund_pubkey_hex,
|
||||
"invoice": payment_request,
|
||||
"referralId": "lnbits",
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
res = res.json()
|
||||
logger.info(
|
||||
f"Boltz - created normal swap, boltz_id: {res['id']}. wallet: {data.wallet}"
|
||||
)
|
||||
return SubmarineSwap(
|
||||
id=swap_id,
|
||||
time=get_timestamp(),
|
||||
wallet=data.wallet,
|
||||
amount=data.amount,
|
||||
payment_hash=payment_hash,
|
||||
refund_privkey=refund_privkey.wif(net),
|
||||
refund_address=data.refund_address,
|
||||
boltz_id=res["id"],
|
||||
status="pending",
|
||||
address=res["address"],
|
||||
expected_amount=res["expectedAmount"],
|
||||
timeout_block_height=res["timeoutBlockHeight"],
|
||||
bip21=res["bip21"],
|
||||
redeem_script=res["redeemScript"],
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
explanation taken from electrum
|
||||
send on Lightning, receive on-chain
|
||||
- User generates preimage, RHASH. Sends RHASH to server.
|
||||
- Server creates an LN invoice for RHASH.
|
||||
- User pays LN invoice - except server needs to hold the HTLC as preimage is unknown.
|
||||
- Server creates on-chain output locked to RHASH.
|
||||
- User spends on-chain output, revealing preimage.
|
||||
- Server fulfills HTLC using preimage.
|
||||
Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.
|
||||
"""
|
||||
|
||||
|
||||
async def create_reverse_swap(
|
||||
data: CreateReverseSubmarineSwap,
|
||||
) -> [ReverseSubmarineSwap, asyncio.Task]:
|
||||
if not check_boltz_limits(data.amount):
|
||||
msg = f"Boltz - reverse swap not in boltz limits"
|
||||
logger.warning(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
swap_id = urlsafe_short_hash()
|
||||
|
||||
if not await check_balance(data):
|
||||
logger.error(f"Boltz - reverse swap, insufficient balance.")
|
||||
return False
|
||||
|
||||
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
|
||||
claim_pubkey_hex = hexlify(claim_privkey.sec()).decode("UTF-8")
|
||||
preimage = os.urandom(32)
|
||||
preimage_hash = sha256(preimage).hexdigest()
|
||||
|
||||
res = req_wrap(
|
||||
"post",
|
||||
f"{settings.boltz_url}/createswap",
|
||||
json={
|
||||
"type": "reversesubmarine",
|
||||
"pairId": "BTC/BTC",
|
||||
"orderSide": "buy",
|
||||
"invoiceAmount": data.amount,
|
||||
"preimageHash": preimage_hash,
|
||||
"claimPublicKey": claim_pubkey_hex,
|
||||
"referralId": "lnbits",
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
res = res.json()
|
||||
|
||||
logger.info(
|
||||
f"Boltz - created reverse swap, boltz_id: {res['id']}. wallet: {data.wallet}"
|
||||
)
|
||||
|
||||
swap = ReverseSubmarineSwap(
|
||||
id=swap_id,
|
||||
amount=data.amount,
|
||||
wallet=data.wallet,
|
||||
onchain_address=data.onchain_address,
|
||||
instant_settlement=data.instant_settlement,
|
||||
claim_privkey=claim_privkey.wif(net),
|
||||
preimage=preimage.hex(),
|
||||
status="pending",
|
||||
boltz_id=res["id"],
|
||||
timeout_block_height=res["timeoutBlockHeight"],
|
||||
lockup_address=res["lockupAddress"],
|
||||
onchain_amount=res["onchainAmount"],
|
||||
redeem_script=res["redeemScript"],
|
||||
invoice=res["invoice"],
|
||||
time=get_timestamp(),
|
||||
)
|
||||
logger.debug(f"Boltz - waiting for onchain tx, reverse swap_id: {swap.id}")
|
||||
task = create_task_log_exception(
|
||||
swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_initial)
|
||||
)
|
||||
return swap, task
|
||||
|
||||
|
||||
def start_onchain_listener(swap: ReverseSubmarineSwap) -> asyncio.Task:
|
||||
return create_task_log_exception(
|
||||
swap.id, wait_for_onchain_tx(swap, swap_websocket_callback_restart)
|
||||
)
|
||||
|
||||
|
||||
async def start_confirmation_listener(
|
||||
swap: ReverseSubmarineSwap, mempool_lockup_tx
|
||||
) -> asyncio.Task:
|
||||
logger.debug(f"Boltz - reverse swap, waiting for confirmation...")
|
||||
|
||||
tx, txid, *_ = mempool_lockup_tx
|
||||
|
||||
confirmed = await wait_for_websocket_message({"track-tx": txid}, "txConfirmed")
|
||||
if confirmed:
|
||||
logger.debug(f"Boltz - reverse swap lockup transaction confirmed! claiming...")
|
||||
await create_claim_tx(swap, mempool_lockup_tx)
|
||||
else:
|
||||
logger.debug(f"Boltz - reverse swap lockup transaction still not confirmed.")
|
||||
|
||||
|
||||
def create_task_log_exception(swap_id: str, awaitable: Awaitable) -> asyncio.Task:
|
||||
async def _log_exception(awaitable):
|
||||
try:
|
||||
return await awaitable
|
||||
except Exception as e:
|
||||
logger.error(f"Boltz - reverse swap failed!: {swap_id} - {e}")
|
||||
await update_swap_status(swap_id, "failed")
|
||||
|
||||
return asyncio.create_task(_log_exception(awaitable))
|
||||
|
||||
|
||||
async def swap_websocket_callback_initial(swap):
|
||||
wstask = asyncio.create_task(
|
||||
wait_for_websocket_message(
|
||||
{"track-address": swap.lockup_address}, "address-transactions"
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
f"Boltz - created task, waiting on mempool websocket for address: {swap.lockup_address}"
|
||||
)
|
||||
|
||||
# create_task is used because pay_invoice is stuck as long as boltz does not
|
||||
# see the onchain claim tx and it ends up in deadlock
|
||||
task: asyncio.Task = create_task_log_exception(
|
||||
swap.id,
|
||||
pay_invoice(
|
||||
wallet_id=swap.wallet,
|
||||
payment_request=swap.invoice,
|
||||
description=f"reverse swap for {swap.amount} sats on boltz.exchange",
|
||||
extra={"tag": "boltz", "swap_id": swap.id, "reverse": True},
|
||||
),
|
||||
)
|
||||
logger.debug(f"Boltz - task pay_invoice created, reverse swap_id: {swap.id}")
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[task, wstask], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
message = done.pop().result()
|
||||
|
||||
# pay_invoice already failed, do not wait for onchain tx anymore
|
||||
if message is None:
|
||||
logger.debug(f"Boltz - pay_invoice already failed cancel websocket task.")
|
||||
wstask.cancel()
|
||||
raise
|
||||
|
||||
return task, message
|
||||
|
||||
|
||||
async def swap_websocket_callback_restart(swap):
|
||||
logger.debug(f"Boltz - swap_websocket_callback_restart called...")
|
||||
message = await wait_for_websocket_message(
|
||||
{"track-address": swap.lockup_address}, "address-transactions"
|
||||
)
|
||||
return None, message
|
||||
|
||||
|
||||
async def wait_for_onchain_tx(swap: ReverseSubmarineSwap, callback):
|
||||
task, txs = await callback(swap)
|
||||
mempool_lockup_tx = get_mempool_tx_from_txs(txs, swap.lockup_address)
|
||||
if mempool_lockup_tx:
|
||||
tx, txid, *_ = mempool_lockup_tx
|
||||
if swap.instant_settlement or tx["status"]["confirmed"]:
|
||||
logger.debug(
|
||||
f"Boltz - reverse swap instant settlement, claiming immediatly..."
|
||||
)
|
||||
await create_claim_tx(swap, mempool_lockup_tx)
|
||||
else:
|
||||
await start_confirmation_listener(swap, mempool_lockup_tx)
|
||||
try:
|
||||
if task:
|
||||
await task
|
||||
except:
|
||||
logger.error(
|
||||
f"Boltz - could not await pay_invoice task, but sent onchain. should never happen!"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Boltz - mempool lockup tx not found.")
|
||||
|
||||
|
||||
async def create_claim_tx(swap: ReverseSubmarineSwap, mempool_lockup_tx):
|
||||
tx = await create_onchain_tx(swap, mempool_lockup_tx)
|
||||
await send_onchain_tx(tx)
|
||||
logger.debug(f"Boltz - onchain tx sent, reverse swap completed")
|
||||
await update_swap_status(swap.id, "complete")
|
||||
|
||||
|
||||
async def create_refund_tx(swap: SubmarineSwap):
|
||||
mempool_lockup_tx = get_mempool_tx(swap.address)
|
||||
tx = await create_onchain_tx(swap, mempool_lockup_tx)
|
||||
await send_onchain_tx(tx)
|
||||
|
||||
|
||||
def check_block_height(block_height: int):
|
||||
current_block_height = get_mempool_blockheight()
|
||||
if current_block_height <= block_height:
|
||||
msg = f"refund not possible, timeout_block_height ({block_height}) is not yet exceeded ({current_block_height})"
|
||||
logger.debug(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
"""
|
||||
a submarine swap consists of 2 onchain tx's a lockup and a redeem tx.
|
||||
we create a tx to redeem the funds locked by the onchain lockup tx.
|
||||
claim tx for reverse swaps, refund tx for normal swaps they are the same
|
||||
onchain redeem tx, the difference between them is the private key, onchain_address,
|
||||
input sequence and input script_sig
|
||||
"""
|
||||
|
||||
|
||||
async def create_onchain_tx(
|
||||
swap: Union[ReverseSubmarineSwap, SubmarineSwap], mempool_lockup_tx
|
||||
) -> Transaction:
|
||||
is_refund_tx = type(swap) == SubmarineSwap
|
||||
if is_refund_tx:
|
||||
check_block_height(swap.timeout_block_height)
|
||||
privkey = ec.PrivateKey.from_wif(swap.refund_privkey)
|
||||
onchain_address = swap.refund_address
|
||||
preimage = b""
|
||||
sequence = 0xFFFFFFFE
|
||||
else:
|
||||
privkey = ec.PrivateKey.from_wif(swap.claim_privkey)
|
||||
preimage = unhexlify(swap.preimage)
|
||||
onchain_address = swap.onchain_address
|
||||
sequence = 0xFFFFFFFF
|
||||
|
||||
locktime = swap.timeout_block_height
|
||||
redeem_script = unhexlify(swap.redeem_script)
|
||||
|
||||
fees = get_fee_estimation()
|
||||
|
||||
tx, txid, vout_cnt, vout_amount = mempool_lockup_tx
|
||||
|
||||
script_pubkey = script.address_to_scriptpubkey(onchain_address)
|
||||
|
||||
vin = [TransactionInput(unhexlify(txid), vout_cnt, sequence=sequence)]
|
||||
vout = [TransactionOutput(vout_amount - fees, script_pubkey)]
|
||||
tx = Transaction(vin=vin, vout=vout)
|
||||
|
||||
if is_refund_tx:
|
||||
tx.locktime = locktime
|
||||
|
||||
# TODO: 2 rounds for fee calculation, look at vbytes after signing and do another TX
|
||||
s = script.Script(data=redeem_script)
|
||||
for i, inp in enumerate(vin):
|
||||
if is_refund_tx:
|
||||
rs = bytes([34]) + bytes([0]) + bytes([32]) + sha256(redeem_script).digest()
|
||||
tx.vin[i].script_sig = script.Script(data=rs)
|
||||
h = tx.sighash_segwit(i, s, vout_amount)
|
||||
sig = privkey.sign(h).serialize() + bytes([SIGHASH.ALL])
|
||||
witness_items = [sig, preimage, redeem_script]
|
||||
tx.vin[i].witness = script.Witness(items=witness_items)
|
||||
|
||||
return tx
|
||||
|
||||
|
||||
def get_swap_status(swap: Union[SubmarineSwap, ReverseSubmarineSwap]) -> SwapStatus:
|
||||
swap_status = SwapStatus(
|
||||
wallet=swap.wallet,
|
||||
swap_id=swap.id,
|
||||
)
|
||||
|
||||
try:
|
||||
boltz_request = get_boltz_status(swap.boltz_id)
|
||||
swap_status.boltz = boltz_request["status"]
|
||||
except httpx.HTTPStatusError as exc:
|
||||
json = exc.response.json()
|
||||
swap_status.boltz = json["error"]
|
||||
if "could not find" in swap_status.boltz:
|
||||
swap_status.exists = False
|
||||
|
||||
if type(swap) == SubmarineSwap:
|
||||
swap_status.reverse = False
|
||||
swap_status.address = swap.address
|
||||
else:
|
||||
swap_status.reverse = True
|
||||
swap_status.address = swap.lockup_address
|
||||
|
||||
swap_status.block_height = get_mempool_blockheight()
|
||||
swap_status.timeout_block_height = (
|
||||
f"{str(swap.timeout_block_height)} -> current: {str(swap_status.block_height)}"
|
||||
)
|
||||
|
||||
if swap_status.block_height >= swap.timeout_block_height:
|
||||
swap_status.hit_timeout = True
|
||||
|
||||
mempool_tx = get_mempool_tx(swap_status.address)
|
||||
swap_status.lockup = mempool_tx
|
||||
if mempool_tx == None:
|
||||
swap_status.has_lockup = False
|
||||
swap_status.confirmed = False
|
||||
swap_status.mempool = "transaction.unknown"
|
||||
swap_status.message = "lockup tx not in mempool"
|
||||
else:
|
||||
swap_status.has_lockup = True
|
||||
tx, *_ = mempool_tx
|
||||
if tx["status"]["confirmed"] == True:
|
||||
swap_status.mempool = "transaction.confirmed"
|
||||
swap_status.confirmed = True
|
||||
else:
|
||||
swap_status.confirmed = False
|
||||
swap_status.mempool = "transaction.unconfirmed"
|
||||
|
||||
return swap_status
|
||||
|
||||
|
||||
def check_boltz_limits(amount):
|
||||
try:
|
||||
pairs = get_boltz_pairs()
|
||||
limits = pairs["pairs"]["BTC/BTC"]["limits"]
|
||||
return amount >= limits["minimal"] and amount <= limits["maximal"]
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def get_boltz_pairs():
|
||||
res = req_wrap(
|
||||
"get",
|
||||
f"{settings.boltz_url}/getpairs",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
return res.json()
|
||||
|
||||
|
||||
def get_boltz_status(boltzid):
|
||||
res = req_wrap(
|
||||
"post",
|
||||
f"{settings.boltz_url}/swapstatus",
|
||||
json={"id": boltzid},
|
||||
)
|
||||
return res.json()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Boltz",
|
||||
"short_description": "Perform onchain/offchain swaps",
|
||||
"icon": "swap_horiz",
|
||||
"tile": "/boltz/static/image/boltz.png",
|
||||
"contributors": ["dni"]
|
||||
}
|
||||
|
||||
+141
-82
@@ -1,21 +1,21 @@
|
||||
from http import HTTPStatus
|
||||
import time
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from boltz_client.boltz import BoltzReverseSwapResponse, BoltzSwapResponse
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import (
|
||||
AutoReverseSubmarineSwap,
|
||||
CreateAutoReverseSubmarineSwap,
|
||||
CreateReverseSubmarineSwap,
|
||||
CreateSubmarineSwap,
|
||||
ReverseSubmarineSwap,
|
||||
SubmarineSwap,
|
||||
)
|
||||
|
||||
"""
|
||||
Submarine Swaps
|
||||
"""
|
||||
|
||||
|
||||
async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[SubmarineSwap]:
|
||||
if isinstance(wallet_ids, str):
|
||||
@@ -30,20 +30,6 @@ async def get_submarine_swaps(wallet_ids: Union[str, List[str]]) -> List[Submari
|
||||
return [SubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_pending_submarine_swaps(
|
||||
wallet_ids: Union[str, List[str]]
|
||||
) -> List[SubmarineSwap]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltz.submarineswap WHERE wallet IN ({q}) and status='pending' order by time DESC",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
return [SubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltz.submarineswap WHERE status='pending' order by time DESC",
|
||||
@@ -51,14 +37,20 @@ async def get_all_pending_submarine_swaps() -> List[SubmarineSwap]:
|
||||
return [SubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_submarine_swap(swap_id) -> SubmarineSwap:
|
||||
async def get_submarine_swap(swap_id) -> Optional[SubmarineSwap]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltz.submarineswap WHERE id = ?", (swap_id,)
|
||||
)
|
||||
return SubmarineSwap(**row) if row else None
|
||||
|
||||
|
||||
async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]:
|
||||
async def create_submarine_swap(
|
||||
data: CreateSubmarineSwap,
|
||||
swap: BoltzSwapResponse,
|
||||
swap_id: str,
|
||||
refund_privkey_wif: str,
|
||||
payment_hash: str,
|
||||
) -> Optional[SubmarineSwap]:
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
@@ -80,26 +72,22 @@ async def create_submarine_swap(swap: SubmarineSwap) -> Optional[SubmarineSwap]:
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
swap_id,
|
||||
data.wallet,
|
||||
payment_hash,
|
||||
"pending",
|
||||
swap.id,
|
||||
swap.wallet,
|
||||
swap.payment_hash,
|
||||
swap.status,
|
||||
swap.boltz_id,
|
||||
swap.refund_privkey,
|
||||
swap.refund_address,
|
||||
swap.expected_amount,
|
||||
swap.timeout_block_height,
|
||||
refund_privkey_wif,
|
||||
data.refund_address,
|
||||
swap.expectedAmount,
|
||||
swap.timeoutBlockHeight,
|
||||
swap.address,
|
||||
swap.bip21,
|
||||
swap.redeem_script,
|
||||
swap.amount,
|
||||
swap.redeemScript,
|
||||
data.amount,
|
||||
),
|
||||
)
|
||||
return await get_submarine_swap(swap.id)
|
||||
|
||||
|
||||
async def delete_submarine_swap(swap_id):
|
||||
await db.execute("DELETE FROM boltz.submarineswap WHERE id = ?", (swap_id,))
|
||||
return await get_submarine_swap(swap_id)
|
||||
|
||||
|
||||
async def get_reverse_submarine_swaps(
|
||||
@@ -117,21 +105,6 @@ async def get_reverse_submarine_swaps(
|
||||
return [ReverseSubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_pending_reverse_submarine_swaps(
|
||||
wallet_ids: Union[str, List[str]]
|
||||
) -> List[ReverseSubmarineSwap]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltz.reverse_submarineswap WHERE wallet IN ({q}) and status='pending' order by time DESC",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
return [ReverseSubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap]:
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltz.reverse_submarineswap WHERE status='pending' order by time DESC"
|
||||
@@ -140,7 +113,7 @@ async def get_all_pending_reverse_submarine_swaps() -> List[ReverseSubmarineSwap
|
||||
return [ReverseSubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap:
|
||||
async def get_reverse_submarine_swap(swap_id) -> Optional[ReverseSubmarineSwap]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltz.reverse_submarineswap WHERE id = ?", (swap_id,)
|
||||
)
|
||||
@@ -148,8 +121,31 @@ async def get_reverse_submarine_swap(swap_id) -> SubmarineSwap:
|
||||
|
||||
|
||||
async def create_reverse_submarine_swap(
|
||||
swap: ReverseSubmarineSwap,
|
||||
) -> Optional[ReverseSubmarineSwap]:
|
||||
data: CreateReverseSubmarineSwap,
|
||||
claim_privkey_wif: str,
|
||||
preimage_hex: str,
|
||||
swap: BoltzReverseSwapResponse,
|
||||
) -> ReverseSubmarineSwap:
|
||||
|
||||
swap_id = urlsafe_short_hash()
|
||||
|
||||
reverse_swap = ReverseSubmarineSwap(
|
||||
id=swap_id,
|
||||
wallet=data.wallet,
|
||||
status="pending",
|
||||
boltz_id=swap.id,
|
||||
instant_settlement=data.instant_settlement,
|
||||
preimage=preimage_hex,
|
||||
claim_privkey=claim_privkey_wif,
|
||||
lockup_address=swap.lockupAddress,
|
||||
invoice=swap.invoice,
|
||||
onchain_amount=swap.onchainAmount,
|
||||
onchain_address=data.onchain_address,
|
||||
timeout_block_height=swap.timeoutBlockHeight,
|
||||
redeem_script=swap.redeemScript,
|
||||
amount=data.amount,
|
||||
time=int(time.time()),
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
@@ -172,36 +168,93 @@ async def create_reverse_submarine_swap(
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
swap.id,
|
||||
reverse_swap.id,
|
||||
reverse_swap.wallet,
|
||||
reverse_swap.status,
|
||||
reverse_swap.boltz_id,
|
||||
reverse_swap.instant_settlement,
|
||||
reverse_swap.preimage,
|
||||
reverse_swap.claim_privkey,
|
||||
reverse_swap.lockup_address,
|
||||
reverse_swap.invoice,
|
||||
reverse_swap.onchain_amount,
|
||||
reverse_swap.onchain_address,
|
||||
reverse_swap.timeout_block_height,
|
||||
reverse_swap.redeem_script,
|
||||
reverse_swap.amount,
|
||||
),
|
||||
)
|
||||
return reverse_swap
|
||||
|
||||
|
||||
async def get_auto_reverse_submarine_swaps(
|
||||
wallet_ids: List[str],
|
||||
) -> List[AutoReverseSubmarineSwap]:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet IN ({q}) order by time DESC",
|
||||
(*wallet_ids,),
|
||||
)
|
||||
return [AutoReverseSubmarineSwap(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_auto_reverse_submarine_swap(
|
||||
swap_id,
|
||||
) -> Optional[AutoReverseSubmarineSwap]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
|
||||
)
|
||||
return AutoReverseSubmarineSwap(**row) if row else None
|
||||
|
||||
|
||||
async def get_auto_reverse_submarine_swap_by_wallet(
|
||||
wallet_id,
|
||||
) -> Optional[AutoReverseSubmarineSwap]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltz.auto_reverse_submarineswap WHERE wallet = ?", (wallet_id,)
|
||||
)
|
||||
return AutoReverseSubmarineSwap(**row) if row else None
|
||||
|
||||
|
||||
async def create_auto_reverse_submarine_swap(
|
||||
swap: CreateAutoReverseSubmarineSwap,
|
||||
) -> Optional[AutoReverseSubmarineSwap]:
|
||||
|
||||
swap_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO boltz.auto_reverse_submarineswap (
|
||||
id,
|
||||
wallet,
|
||||
onchain_address,
|
||||
instant_settlement,
|
||||
balance,
|
||||
amount
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
swap_id,
|
||||
swap.wallet,
|
||||
swap.status,
|
||||
swap.boltz_id,
|
||||
swap.instant_settlement,
|
||||
swap.preimage,
|
||||
swap.claim_privkey,
|
||||
swap.lockup_address,
|
||||
swap.invoice,
|
||||
swap.onchain_amount,
|
||||
swap.onchain_address,
|
||||
swap.timeout_block_height,
|
||||
swap.redeem_script,
|
||||
swap.instant_settlement,
|
||||
swap.balance,
|
||||
swap.amount,
|
||||
),
|
||||
)
|
||||
return await get_reverse_submarine_swap(swap.id)
|
||||
return await get_auto_reverse_submarine_swap(swap_id)
|
||||
|
||||
|
||||
async def delete_auto_reverse_submarine_swap(swap_id):
|
||||
await db.execute(
|
||||
"DELETE FROM boltz.auto_reverse_submarineswap WHERE id = ?", (swap_id,)
|
||||
)
|
||||
|
||||
|
||||
async def update_swap_status(swap_id: str, status: str):
|
||||
|
||||
reverse = ""
|
||||
swap = await get_submarine_swap(swap_id)
|
||||
if swap is None:
|
||||
swap = await get_reverse_submarine_swap(swap_id)
|
||||
|
||||
if swap is None:
|
||||
return None
|
||||
|
||||
if type(swap) == SubmarineSwap:
|
||||
if swap:
|
||||
await db.execute(
|
||||
"UPDATE boltz.submarineswap SET status='"
|
||||
+ status
|
||||
@@ -209,17 +262,23 @@ async def update_swap_status(swap_id: str, status: str):
|
||||
+ swap.id
|
||||
+ "'"
|
||||
)
|
||||
if type(swap) == ReverseSubmarineSwap:
|
||||
reverse = "reverse"
|
||||
logger.info(
|
||||
f"Boltz - swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
|
||||
)
|
||||
return swap
|
||||
|
||||
reverse_swap = await get_reverse_submarine_swap(swap_id)
|
||||
if reverse_swap:
|
||||
await db.execute(
|
||||
"UPDATE boltz.reverse_submarineswap SET status='"
|
||||
+ status
|
||||
+ "' WHERE id='"
|
||||
+ swap.id
|
||||
+ reverse_swap.id
|
||||
+ "'"
|
||||
)
|
||||
logger.info(
|
||||
f"Boltz - reverse swap status change: {status}. boltz_id: {reverse_swap.boltz_id}, wallet: {reverse_swap.wallet}"
|
||||
)
|
||||
return reverse_swap
|
||||
|
||||
message = f"Boltz - {reverse} swap status change: {status}. boltz_id: {swap.boltz_id}, wallet: {swap.wallet}"
|
||||
logger.info(message)
|
||||
|
||||
return swap
|
||||
return None
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
from binascii import hexlify
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
from embit.transaction import Transaction
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .utils import req_wrap
|
||||
|
||||
websocket_url = f"{settings.boltz_mempool_space_url_ws}/api/v1/ws"
|
||||
|
||||
|
||||
async def wait_for_websocket_message(send, message_string):
|
||||
async for websocket in websockets.connect(websocket_url):
|
||||
try:
|
||||
await websocket.send(json.dumps({"action": "want", "data": ["blocks"]}))
|
||||
await websocket.send(json.dumps(send))
|
||||
async for raw in websocket:
|
||||
message = json.loads(raw)
|
||||
if message_string in message:
|
||||
return message.get(message_string)
|
||||
except websockets.ConnectionClosed:
|
||||
continue
|
||||
|
||||
|
||||
def get_mempool_tx(address):
|
||||
res = req_wrap(
|
||||
"get",
|
||||
f"{settings.boltz_mempool_space_url}/api/address/{address}/txs",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
txs = res.json()
|
||||
return get_mempool_tx_from_txs(txs, address)
|
||||
|
||||
|
||||
def get_mempool_tx_from_txs(txs, address):
|
||||
if len(txs) == 0:
|
||||
return None
|
||||
tx = txid = vout_cnt = vout_amount = None
|
||||
for a_tx in txs:
|
||||
for i, vout in enumerate(a_tx["vout"]):
|
||||
if vout["scriptpubkey_address"] == address:
|
||||
tx = a_tx
|
||||
txid = a_tx["txid"]
|
||||
vout_cnt = i
|
||||
vout_amount = vout["value"]
|
||||
# should never happen
|
||||
if tx == None:
|
||||
raise Exception("mempool tx not found")
|
||||
if txid == None:
|
||||
raise Exception("mempool txid not found")
|
||||
return tx, txid, vout_cnt, vout_amount
|
||||
|
||||
|
||||
def get_fee_estimation() -> int:
|
||||
# TODO: hardcoded maximum tx size, in the future we try to get the size of the tx via embit
|
||||
# we need a function like Transaction.vsize()
|
||||
tx_size_vbyte = 200
|
||||
mempool_fees = get_mempool_fees()
|
||||
return mempool_fees * tx_size_vbyte
|
||||
|
||||
|
||||
def get_mempool_fees() -> int:
|
||||
res = req_wrap(
|
||||
"get",
|
||||
f"{settings.boltz_mempool_space_url}/api/v1/fees/recommended",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
fees = res.json()
|
||||
return int(fees["economyFee"])
|
||||
|
||||
|
||||
def get_mempool_blockheight() -> int:
|
||||
res = req_wrap(
|
||||
"get",
|
||||
f"{settings.boltz_mempool_space_url}/api/blocks/tip/height",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
return int(res.text)
|
||||
|
||||
|
||||
async def send_onchain_tx(tx: Transaction):
|
||||
raw = hexlify(tx.serialize())
|
||||
logger.debug(f"Boltz - mempool sending onchain tx...")
|
||||
req_wrap(
|
||||
"post",
|
||||
f"{settings.boltz_mempool_space_url}/api/tx",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
content=raw,
|
||||
)
|
||||
@@ -44,3 +44,21 @@ async def m001_initial(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_auto_swaps(db):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE boltz.auto_reverse_submarineswap (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
onchain_address TEXT NOT NULL,
|
||||
amount INT NOT NULL,
|
||||
balance INT NOT NULL,
|
||||
instant_settlement BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi.params import Query
|
||||
from pydantic.main import BaseModel
|
||||
from sqlalchemy.engine import base # type: ignore
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SubmarineSwap(BaseModel):
|
||||
@@ -24,9 +20,9 @@ class SubmarineSwap(BaseModel):
|
||||
|
||||
|
||||
class CreateSubmarineSwap(BaseModel):
|
||||
wallet: str = Query(...) # type: ignore
|
||||
refund_address: str = Query(...) # type: ignore
|
||||
amount: int = Query(...) # type: ignore
|
||||
wallet: str = Query(...)
|
||||
refund_address: str = Query(...)
|
||||
amount: int = Query(...)
|
||||
|
||||
|
||||
class ReverseSubmarineSwap(BaseModel):
|
||||
@@ -48,28 +44,25 @@ class ReverseSubmarineSwap(BaseModel):
|
||||
|
||||
|
||||
class CreateReverseSubmarineSwap(BaseModel):
|
||||
wallet: str = Query(...) # type: ignore
|
||||
amount: int = Query(...) # type: ignore
|
||||
instant_settlement: bool = Query(...) # type: ignore
|
||||
# validate on-address, bcrt1 for regtest addresses
|
||||
onchain_address: str = Query(
|
||||
..., regex="^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$"
|
||||
) # type: ignore
|
||||
wallet: str = Query(...)
|
||||
amount: int = Query(...)
|
||||
instant_settlement: bool = Query(...)
|
||||
onchain_address: str = Query(...)
|
||||
|
||||
|
||||
class SwapStatus(BaseModel):
|
||||
swap_id: str
|
||||
class AutoReverseSubmarineSwap(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
status: str = ""
|
||||
message: str = ""
|
||||
boltz: str = ""
|
||||
mempool: str = ""
|
||||
address: str = ""
|
||||
block_height: int = 0
|
||||
timeout_block_height: str = ""
|
||||
lockup: Optional[dict] = {}
|
||||
has_lockup: bool = False
|
||||
hit_timeout: bool = False
|
||||
confirmed: bool = True
|
||||
exists: bool = True
|
||||
reverse: bool = False
|
||||
amount: int
|
||||
balance: int
|
||||
onchain_address: str
|
||||
instant_settlement: bool
|
||||
time: int
|
||||
|
||||
|
||||
class CreateAutoReverseSubmarineSwap(BaseModel):
|
||||
wallet: str = Query(...)
|
||||
amount: int = Query(...)
|
||||
balance: int = Query(0)
|
||||
instant_settlement: bool = Query(...)
|
||||
onchain_address: str = Query(...)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
+147
-121
@@ -1,129 +1,25 @@
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
from boltz_client.boltz import BoltzNotFoundException, BoltzSwapStatusException
|
||||
from boltz_client.mempool import MempoolBlockHeightException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import check_transaction_status
|
||||
from lnbits.core.services import check_transaction_status, fee_reserve
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .boltz import (
|
||||
create_claim_tx,
|
||||
create_refund_tx,
|
||||
get_swap_status,
|
||||
start_confirmation_listener,
|
||||
start_onchain_listener,
|
||||
)
|
||||
from .crud import (
|
||||
create_reverse_submarine_swap,
|
||||
get_all_pending_reverse_submarine_swaps,
|
||||
get_all_pending_submarine_swaps,
|
||||
get_reverse_submarine_swap,
|
||||
get_auto_reverse_submarine_swap_by_wallet,
|
||||
get_submarine_swap,
|
||||
update_swap_status,
|
||||
)
|
||||
|
||||
"""
|
||||
testcases for boltz startup
|
||||
A. normal swaps
|
||||
1. test: create -> kill -> start -> startup invoice listeners -> pay onchain funds -> should complete
|
||||
2. test: create -> kill -> pay onchain funds -> start -> startup check -> should complete
|
||||
3. test: create -> kill -> mine blocks and hit timeout -> start -> should go timeout/failed
|
||||
4. test: create -> kill -> pay to less onchain funds -> mine blocks hit timeout -> start lnbits -> should be refunded
|
||||
|
||||
B. reverse swaps
|
||||
1. test: create instant -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should claim/complete
|
||||
2. test: create instant -> kill -> no lockup -> start lnbits -> should start onchain listener -> boltz does lockup -> should claim/complete (difficult to test)
|
||||
3. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should start tx listener -> after confirmation -> should claim/complete
|
||||
4. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete
|
||||
5. test: create -> kill -> boltz does lockup -> hit timeout -> boltz refunds -> start -> should timeout
|
||||
"""
|
||||
|
||||
|
||||
async def check_for_pending_swaps():
|
||||
try:
|
||||
swaps = await get_all_pending_submarine_swaps()
|
||||
reverse_swaps = await get_all_pending_reverse_submarine_swaps()
|
||||
if len(swaps) > 0 or len(reverse_swaps) > 0:
|
||||
logger.debug(f"Boltz - startup swap check")
|
||||
except:
|
||||
# database is not created yet, do nothing
|
||||
return
|
||||
|
||||
if len(swaps) > 0:
|
||||
logger.debug(f"Boltz - {len(swaps)} pending swaps")
|
||||
for swap in swaps:
|
||||
try:
|
||||
swap_status = get_swap_status(swap)
|
||||
# should only happen while development when regtest is reset
|
||||
if swap_status.exists is False:
|
||||
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||
await update_swap_status(swap.id, "failed")
|
||||
continue
|
||||
|
||||
payment_status = await check_transaction_status(
|
||||
swap.wallet, swap.payment_hash
|
||||
)
|
||||
|
||||
if payment_status.paid:
|
||||
logger.debug(
|
||||
f"Boltz - swap: {swap.boltz_id} got paid while offline."
|
||||
)
|
||||
await update_swap_status(swap.id, "complete")
|
||||
else:
|
||||
if swap_status.hit_timeout:
|
||||
if not swap_status.has_lockup:
|
||||
logger.debug(
|
||||
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
||||
)
|
||||
await update_swap_status(swap.id, "timeout")
|
||||
else:
|
||||
logger.debug(f"Boltz - refunding swap: {swap.id}...")
|
||||
await create_refund_tx(swap)
|
||||
await update_swap_status(swap.id, "refunded")
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Boltz - swap: {swap.id} - {str(exc)}")
|
||||
|
||||
if len(reverse_swaps) > 0:
|
||||
logger.debug(f"Boltz - {len(reverse_swaps)} pending reverse swaps")
|
||||
for reverse_swap in reverse_swaps:
|
||||
try:
|
||||
swap_status = get_swap_status(reverse_swap)
|
||||
|
||||
if swap_status.exists is False:
|
||||
logger.debug(
|
||||
f"Boltz - reverse_swap: {reverse_swap.boltz_id} does not exist."
|
||||
)
|
||||
await update_swap_status(reverse_swap.id, "failed")
|
||||
continue
|
||||
|
||||
# if timeout hit, boltz would have already refunded
|
||||
if swap_status.hit_timeout:
|
||||
logger.debug(
|
||||
f"Boltz - reverse_swap: {reverse_swap.boltz_id} timeout."
|
||||
)
|
||||
await update_swap_status(reverse_swap.id, "timeout")
|
||||
continue
|
||||
|
||||
if not swap_status.has_lockup:
|
||||
# start listener for onchain address
|
||||
logger.debug(
|
||||
f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted onchain address listener."
|
||||
)
|
||||
await start_onchain_listener(reverse_swap)
|
||||
continue
|
||||
|
||||
if reverse_swap.instant_settlement or swap_status.confirmed:
|
||||
await create_claim_tx(reverse_swap, swap_status.lockup)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Boltz - reverse_swap: {reverse_swap.boltz_id} restarted confirmation listener."
|
||||
)
|
||||
await start_confirmation_listener(reverse_swap, swap_status.lockup)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Boltz - reverse swap: {reverse_swap.id} - {str(exc)}")
|
||||
from .models import CreateReverseSubmarineSwap, ReverseSubmarineSwap, SubmarineSwap
|
||||
from .utils import create_boltz_client, execute_reverse_swap
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
@@ -136,19 +32,149 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "boltz" != payment.extra.get("tag"):
|
||||
|
||||
await check_for_auto_swap(payment)
|
||||
|
||||
if payment.extra.get("tag") != "boltz":
|
||||
# not a boltz invoice
|
||||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
swap_id = payment.extra.get("swap_id")
|
||||
swap = await get_submarine_swap(swap_id)
|
||||
|
||||
if not swap:
|
||||
logger.error(f"swap_id: {swap_id} not found.")
|
||||
if payment.extra:
|
||||
swap_id = payment.extra.get("swap_id")
|
||||
if swap_id:
|
||||
swap = await get_submarine_swap(swap_id)
|
||||
if swap:
|
||||
await update_swap_status(swap_id, "complete")
|
||||
|
||||
|
||||
async def check_for_auto_swap(payment: Payment) -> None:
|
||||
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(payment.wallet_id)
|
||||
if auto_swap:
|
||||
wallet = await get_wallet(payment.wallet_id)
|
||||
if wallet:
|
||||
reserve = fee_reserve(wallet.balance_msat) / 1000
|
||||
balance = wallet.balance_msat / 1000
|
||||
amount = balance - auto_swap.balance - reserve
|
||||
if amount >= auto_swap.amount:
|
||||
|
||||
client = create_boltz_client()
|
||||
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
|
||||
amount=int(amount)
|
||||
)
|
||||
new_swap = await create_reverse_submarine_swap(
|
||||
CreateReverseSubmarineSwap(
|
||||
wallet=auto_swap.wallet,
|
||||
amount=int(amount),
|
||||
instant_settlement=auto_swap.instant_settlement,
|
||||
onchain_address=auto_swap.onchain_address,
|
||||
),
|
||||
claim_privkey_wif,
|
||||
preimage_hex,
|
||||
swap,
|
||||
)
|
||||
await execute_reverse_swap(client, new_swap)
|
||||
|
||||
logger.info(
|
||||
f"Boltz: auto reverse swap created with amount: {amount}, boltz_id: {new_swap.boltz_id}"
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
testcases for boltz startup
|
||||
A. normal swaps
|
||||
1. test: create -> kill -> start -> startup invoice listeners -> pay onchain funds -> should complete
|
||||
2. test: create -> kill -> pay onchain funds -> mine block -> start -> startup check -> should complete
|
||||
3. test: create -> kill -> mine blocks and hit timeout -> start -> should go timeout/failed
|
||||
4. test: create -> kill -> pay to less onchain funds -> mine blocks hit timeout -> start lnbits -> should be refunded
|
||||
|
||||
B. reverse swaps
|
||||
1. test: create instant -> kill -> boltz does lockup -> not confirmed -> start lnbits -> should claim/complete
|
||||
2. test: create -> kill -> boltz does lockup -> not confirmed -> start lnbits -> mine blocks -> should claim/complete
|
||||
3. test: create -> kill -> boltz does lockup -> confirmed -> start lnbits -> should claim/complete
|
||||
"""
|
||||
|
||||
|
||||
async def check_for_pending_swaps():
|
||||
try:
|
||||
swaps = await get_all_pending_submarine_swaps()
|
||||
reverse_swaps = await get_all_pending_reverse_submarine_swaps()
|
||||
if len(swaps) > 0 or len(reverse_swaps) > 0:
|
||||
logger.debug(f"Boltz - startup swap check")
|
||||
except:
|
||||
logger.error(
|
||||
f"Boltz - startup swap check, database is not created yet, do nothing"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Boltz - lightning invoice is paid, normal swap completed. swap_id: {swap_id}"
|
||||
)
|
||||
await update_swap_status(swap_id, "complete")
|
||||
client = create_boltz_client()
|
||||
|
||||
if len(swaps) > 0:
|
||||
logger.debug(f"Boltz - {len(swaps)} pending swaps")
|
||||
for swap in swaps:
|
||||
await check_swap(swap, client)
|
||||
|
||||
if len(reverse_swaps) > 0:
|
||||
logger.debug(f"Boltz - {len(reverse_swaps)} pending reverse swaps")
|
||||
for reverse_swap in reverse_swaps:
|
||||
await check_reverse_swap(reverse_swap, client)
|
||||
|
||||
|
||||
async def check_swap(swap: SubmarineSwap, client):
|
||||
try:
|
||||
payment_status = await check_transaction_status(swap.wallet, swap.payment_hash)
|
||||
if payment_status.paid:
|
||||
logger.debug(f"Boltz - swap: {swap.boltz_id} got paid while offline.")
|
||||
await update_swap_status(swap.id, "complete")
|
||||
else:
|
||||
try:
|
||||
_ = client.swap_status(swap.id)
|
||||
except:
|
||||
txs = client.mempool.get_txs_from_address(swap.address)
|
||||
if len(txs) == 0:
|
||||
await update_swap_status(swap.id, "timeout")
|
||||
else:
|
||||
await client.refund_swap(
|
||||
privkey_wif=swap.refund_privkey,
|
||||
lockup_address=swap.address,
|
||||
receive_address=swap.refund_address,
|
||||
redeem_script_hex=swap.redeem_script,
|
||||
timeout_block_height=swap.timeout_block_height,
|
||||
)
|
||||
await update_swap_status(swap.id, "refunded")
|
||||
except BoltzNotFoundException as exc:
|
||||
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||
await update_swap_status(swap.id, "failed")
|
||||
except MempoolBlockHeightException as exc:
|
||||
logger.debug(
|
||||
f"Boltz - tried to refund swap: {swap.id}, but has not reached the timeout."
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"Boltz - unhandled exception, swap: {swap.id} - {str(exc)}")
|
||||
|
||||
|
||||
async def check_reverse_swap(reverse_swap: ReverseSubmarineSwap, client):
|
||||
try:
|
||||
_ = client.swap_status(reverse_swap.boltz_id)
|
||||
await client.claim_reverse_swap(
|
||||
lockup_address=reverse_swap.lockup_address,
|
||||
receive_address=reverse_swap.onchain_address,
|
||||
privkey_wif=reverse_swap.claim_privkey,
|
||||
preimage_hex=reverse_swap.preimage,
|
||||
redeem_script_hex=reverse_swap.redeem_script,
|
||||
zeroconf=reverse_swap.instant_settlement,
|
||||
)
|
||||
await update_swap_status(reverse_swap.id, "complete")
|
||||
|
||||
except BoltzSwapStatusException as exc:
|
||||
logger.debug(f"Boltz - swap_status: {str(exc)}")
|
||||
await update_swap_status(reverse_swap.id, "failed")
|
||||
# should only happen while development when regtest is reset
|
||||
except BoltzNotFoundException as exc:
|
||||
logger.debug(f"Boltz - reverse swap: {reverse_swap.boltz_id} does not exist.")
|
||||
await update_swap_status(reverse_swap.id, "failed")
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"Boltz - unhandled exception, reverse swap: {reverse_swap.id} - {str(exc)}"
|
||||
)
|
||||
|
||||
@@ -1,236 +1,93 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="About Boltz"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<img src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg" alt="" />
|
||||
<img
|
||||
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
|
||||
style="padding: 5px 9px"
|
||||
alt=""
|
||||
/>
|
||||
<p><b>NON CUSTODIAL atomic swap service</b></p>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Providing trustless and account-free swap services since 2018. Move IN and
|
||||
OUT of the lightning network and remain in control of your bitcoin, at all
|
||||
time.
|
||||
</h5>
|
||||
<p>
|
||||
Link:
|
||||
<a target="_blank" href="https://boltz.exchange"
|
||||
>https://boltz.exchange
|
||||
</a>
|
||||
<br />
|
||||
README:
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||
>read more</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<small
|
||||
>Extension created by,
|
||||
<a target="_blank" href="https://github.com/dni">dni</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="text-subtitle1 q-my-none">
|
||||
<b>Fee Information</b>
|
||||
</h3>
|
||||
<span>
|
||||
Every swap consists of 2 onchain transactions, lockup and claim / refund,
|
||||
routing fees and a Boltz fee of <b>0.5%</b>.
|
||||
</span>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Fee example: Lightning -> Onchain"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card-section>
|
||||
<img
|
||||
src="https://boltz.exchange/static/media/Shape.6c1a92b3.svg"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="https://boltz.exchange/static/media/Boltz.02fb7acb.svg"
|
||||
style="padding: 5px 9px"
|
||||
alt=""
|
||||
/>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Boltz.exchange: Do onchain to offchain and vice-versa swaps
|
||||
</h5>
|
||||
You want to swap out 100.000 sats, Lightning to Onchain:
|
||||
<ul style="padding-left: 12px">
|
||||
<li>Onchain lockup tx fee: ~500 sats</li>
|
||||
<li>Onchain claim tx fee: 1000 sats (hardcoded)</li>
|
||||
<li>Routing fees (paid by you): unknown</li>
|
||||
<li>Boltz fees: 500 sats</li>
|
||||
<li>Fees total: 2000 sats + routing fees</li>
|
||||
<li>You receive: 98.000 sats</li>
|
||||
</ul>
|
||||
<p>
|
||||
Submarine and Reverse Submarine Swaps on LNbits via boltz.exchange
|
||||
API<br />
|
||||
</p>
|
||||
<p>
|
||||
Link :
|
||||
<a target="_blank" href="https://boltz.exchange"
|
||||
>https://boltz.exchange
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||
>More details</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<small
|
||||
>Created by,
|
||||
<a target="_blank" href="https://github.com/dni">dni</a></small
|
||||
>
|
||||
onchain_amount_received = amount - (amount * boltz_fee / 100) -
|
||||
lockup_fee - claim_fee
|
||||
</p>
|
||||
<p>98.000 = 100.000 - 500 - 500 - 1000</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET swap/reverse">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/boltz/api/v1/swap/reverse</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON list of reverse submarine swaps</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key:
|
||||
{{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST swap/reverse"
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Fee example: Onchain -> Lightning"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">POST</span>
|
||||
/boltz/api/v1/swap/reverse</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"wallet": <string>, "onchain_address": <string>,
|
||||
"amount": <integer>, "instant_settlement":
|
||||
<boolean>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON create a reverse-submarine swaps</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ root_url }}/boltz/api/v1/swap/reverse -H "X-Api-Key:
|
||||
{{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card-section>
|
||||
You want to swap in 100.000 sats, Onchain to Lightning:
|
||||
<ul style="padding-left: 12px">
|
||||
<li>Onchain lockup tx fee: whatever you choose when paying</li>
|
||||
<li>Onchain claim tx fee: ~500 sats</li>
|
||||
<li>Routing fees (paid by boltz): unknown</li>
|
||||
<li>Boltz fees: 500 sats (0.5%)</li>
|
||||
<li>Fees total: 1000 sats + lockup_fee</li>
|
||||
<li>You pay onchain: 101.000 sats + lockup_fee</li>
|
||||
<li>You receive lightning: 100.000 sats</li>
|
||||
</ul>
|
||||
<p>
|
||||
onchain_payment + lockup_fee = amount + (amount * boltz_fee / 100) +
|
||||
claim_fee + lockup_fee
|
||||
</p>
|
||||
<p>101.000 + lockup_fee = 100.000 + 500 + 500 + lockup_fee</p>
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET swap">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-blue">GET</span> /boltz/api/v1/swap</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON list of submarine swaps</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="POST swap">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">POST</span> /boltz/api/v1/swap</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"wallet": <string>, "refund_address": <string>,
|
||||
"amount": <integer>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON create a submarine swaps</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ root_url }}/boltz/api/v1/swap -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET swap/refund">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">POST</span>
|
||||
/boltz/api/v1/swap/refund/{swap_id}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON submarine swap</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/refund/{swap_id} -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET swap/status">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">POST</span>
|
||||
/boltz/api/v1/swap/status/{swap_id}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (text/plain)
|
||||
</h5>
|
||||
<code>swap status</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/status/{swap_id} -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET swap/check">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/boltz/api/v1/swap/check</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>JSON pending swaps</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/check -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET boltz-config">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/boltz/api/v1/swap/boltz</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (text/plain)
|
||||
</h5>
|
||||
<code>JSON boltz config</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/boltz -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET mempool-url">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/boltz/api/v1/swap/mempool</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (text/plain)
|
||||
</h5>
|
||||
<code>mempool url</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ root_url }}/boltz/api/v1/swap/mempool -H "X-Api-Key:
|
||||
{{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
</q-card>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<q-dialog v-model="autoReverseSubmarineSwapDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendAutoReverseSubmarineSwapFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="autoReverseSubmarineSwapDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
:disable="autoReverseSubmarineSwapDialog.data.id ? true : false"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
label="Balance to kept + fee_reserve"
|
||||
v-model="autoReverseSubmarineSwapDialog.data.balance"
|
||||
type="number"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
mininum balance kept in wallet after a swap + the fee_reserve
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
:label="amountLabel()"
|
||||
v-model.trim="autoReverseSubmarineSwapDialog.data.amount"
|
||||
type="number"
|
||||
></q-input>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-checkbox
|
||||
v-model="autoReverseSubmarineSwapDialog.data.instant_settlement"
|
||||
value="false"
|
||||
label="Instant settlement"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Create Onchain TX when transaction is in mempool, but not
|
||||
confirmed yet.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="autoReverseSubmarineSwapDialog.data.onchain_address"
|
||||
type="string"
|
||||
label="Onchain address to receive funds"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="autoReverseSubmarineSwapDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Swap"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableAutoReverseSubmarineSwapDialog()"
|
||||
type="submit"
|
||||
label="Create Auto Reverse Swap (Out)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
@click="resetAutoReverseSubmarineSwapDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,54 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Auto Lightning -> Onchain</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportAutoReverseSubmarineSwapCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="autoReverseSubmarineSwaps"
|
||||
row-key="id"
|
||||
:columns="autoReverseSubmarineSwapTable.columns"
|
||||
:pagination.sync="autoReverseSubmarineSwapTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="delete"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="deleteAutoReverseSwap(props.row.id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>delete the automatic reverse swap</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -0,0 +1,35 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
label="Onchain -> Lightning"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="submarineSwapDialog.show = true"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Send onchain funds offchain (BTC -> LN)
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
label="Lightning -> Onchain"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="reverseSubmarineSwapDialog.show = true"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Send offchain funds to onchain address (LN -> BTC)
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
label="Auto (Lightning -> Onchain)"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="autoReverseSubmarineSwapDialog.show = true"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Automatically send offchain funds to onchain address (LN -> BTC) with a
|
||||
predefined threshold
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -0,0 +1,113 @@
|
||||
<q-dialog v-model="checkSwapDialog.show" maximized position="top">
|
||||
<q-card v-if="checkSwapDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<h5>pending swaps</h5>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="checkSwapDialog.data.swaps"
|
||||
row-key="id"
|
||||
:columns="allStatusTable.columns"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="cached"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="refundSwap(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>refund swap</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="download"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="downloadRefundFile(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>dowload refund file</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
<h5>pending reverse swaps</h5>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="checkSwapDialog.data.reverse_swaps"
|
||||
row-key="id"
|
||||
:columns="allStatusTable.columns"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,31 @@
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.bip21"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div>
|
||||
{% raw %}
|
||||
<b>Bitcoin On-Chain TX</b><br />
|
||||
<b>Expected amount (sats): </b> {{ qrCodeDialog.data.expected_amount }}
|
||||
<br />
|
||||
<b>Expected amount (btc): </b> {{ qrCodeDialog.data.expected_amount_btc }}
|
||||
<br />
|
||||
<b>Onchain Address: </b> {{ qrCodeDialog.data.address }} <br />
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.address, 'Onchain address copied to clipboard!')"
|
||||
class="q-ml-sm"
|
||||
>Copy On-Chain Address</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,72 @@
|
||||
<q-dialog v-model="reverseSubmarineSwapDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendReverseSubmarineSwapFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="reverseSubmarineSwapDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
:disable="reverseSubmarineSwapDialog.data.id ? true : false"
|
||||
>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
:label="amountLabel()"
|
||||
v-model.trim="reverseSubmarineSwapDialog.data.amount"
|
||||
type="number"
|
||||
></q-input>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-checkbox
|
||||
v-model="reverseSubmarineSwapDialog.data.instant_settlement"
|
||||
value="false"
|
||||
label="Instant settlement"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Create Onchain TX when transaction is in mempool, but not
|
||||
confirmed yet.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="reverseSubmarineSwapDialog.data.onchain_address"
|
||||
type="string"
|
||||
label="Onchain address to receive funds"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="reverseSubmarineSwapDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Swap"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableReverseSubmarineSwapDialog()"
|
||||
type="submit"
|
||||
label="Create Reverse Swap (OUT)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
@click="resetReverseSubmarineSwapDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,66 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Lightning -> Onchain</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportReverseSubmarineSwapCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="reverseSubmarineSwaps"
|
||||
row-key="id"
|
||||
:columns="reverseSubmarineSwapTable.columns"
|
||||
:pagination.sync="reverseSubmarineSwapTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="info"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openStatusDialog(props.row.id, true)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open swap status info</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -0,0 +1,29 @@
|
||||
<q-dialog v-model="statusDialog.show" position="top">
|
||||
<q-card v-if="statusDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<div>
|
||||
{% raw %}
|
||||
<b>Status: </b> {{ statusDialog.data.status }} <br />
|
||||
<br />
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="refundSwap(statusDialog.data.swap_id)"
|
||||
v-if="!statusDialog.data.reverse"
|
||||
class="q-ml-sm"
|
||||
>Refund
|
||||
</q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="downloadRefundFile(statusDialog.data.swap_id)"
|
||||
v-if="!statusDialog.data.reverse"
|
||||
class="q-ml-sm"
|
||||
>Download refundfile</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,58 @@
|
||||
<q-dialog v-model="submarineSwapDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendSubmarineSwapFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="submarineSwapDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
:disable="submarineSwapDialog.data.id ? true : false"
|
||||
>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="submarineSwapDialog.data.amount"
|
||||
:label="amountLabel()"
|
||||
type="number"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="submarineSwapDialog.data.refund_address"
|
||||
type="string"
|
||||
label="Onchain address to receive funds if swap fails"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="submarineSwapDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Swap"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableSubmarineSwapDialog()"
|
||||
type="submit"
|
||||
label="Create Swap (IN)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
@click="resetSubmarineSwapDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -0,0 +1,78 @@
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Onchain -> Lightning</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="submarineSwaps"
|
||||
row-key="id"
|
||||
:columns="submarineSwapTable.columns"
|
||||
:pagination.sync="submarineSwapTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open swap onchain details</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="info"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openStatusDialog(props.row.id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open swap status info</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.id)"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -1,531 +1,19 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
label="Swap (In)"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="submarineSwapDialog.show = true"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Send onchain funds offchain (BTC -> LN)
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
label="Reverse Swap (Out)"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="reverseSubmarineSwapDialog.show = true"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Send offchain funds to onchain address (LN -> BTC)
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
label="Check Swaps"
|
||||
icon="cached"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="checkSwaps"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Check all pending swaps if they can be refunded.
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Swaps (In)</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportSubmarineSwapCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="submarineSwaps"
|
||||
row-key="id"
|
||||
:columns="submarineSwapTable.columns"
|
||||
:pagination.sync="submarineSwapTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open swap onchain details</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="info"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openStatusDialog(props.row.id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open swap status info</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Reverse Swaps (Out)</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportReverseSubmarineSwapCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="reverseSubmarineSwaps"
|
||||
row-key="id"
|
||||
:columns="reverseSubmarineSwapTable.columns"
|
||||
:pagination.sync="reverseSubmarineSwapTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="info"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openStatusDialog(props.row.id, true)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open swap status info</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
{% include "boltz/_buttons.html" %} {% include
|
||||
"boltz/_submarineSwapList.html" %} {% include
|
||||
"boltz/_reverseSubmarineSwapList.html" %} {% include
|
||||
"boltz/_autoReverseSwapList.html" %}
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Boltz extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "boltz/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||
{% include "boltz/_api_docs.html" %}
|
||||
</div>
|
||||
<q-dialog v-model="submarineSwapDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendSubmarineSwapFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="submarineSwapDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
:disable="submarineSwapDialog.data.id ? true : false"
|
||||
>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="submarineSwapDialog.data.amount"
|
||||
:label="amountLabel()"
|
||||
type="number"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="submarineSwapDialog.data.refund_address"
|
||||
type="string"
|
||||
label="Onchain address to receive funds if swap fails"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="submarineSwapDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Swap"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableSubmarineSwapDialog()"
|
||||
type="submit"
|
||||
label="Create Swap (IN)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
@click="resetSubmarineSwapDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="reverseSubmarineSwapDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendReverseSubmarineSwapFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="reverseSubmarineSwapDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
:disable="reverseSubmarineSwapDialog.data.id ? true : false"
|
||||
>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
:label="amountLabel()"
|
||||
v-model.trim="reverseSubmarineSwapDialog.data.amount"
|
||||
type="number"
|
||||
></q-input>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-checkbox
|
||||
v-model="reverseSubmarineSwapDialog.data.instant_settlement"
|
||||
value="false"
|
||||
label="Instant settlement"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
Create Onchain TX when transaction is in mempool, but not
|
||||
confirmed yet.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="reverseSubmarineSwapDialog.data.onchain_address"
|
||||
type="string"
|
||||
label="Onchain address to receive funds"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="reverseSubmarineSwapDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Swap"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="disableReverseSubmarineSwapDialog()"
|
||||
type="submit"
|
||||
label="Create Reverse Swap (OUT)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
@click="resetReverseSubmarineSwapDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.bip21"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div>
|
||||
{% raw %}
|
||||
<b>Bitcoin On-Chain TX</b><br />
|
||||
<b>Expected amount (sats): </b> {{ qrCodeDialog.data.expected_amount }}
|
||||
<br />
|
||||
<b>Expected amount (btc): </b> {{ qrCodeDialog.data.expected_amount_btc
|
||||
}} <br />
|
||||
<b>Onchain Address: </b> {{ qrCodeDialog.data.address }} <br />
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.address, 'Onchain address copied to clipboard!')"
|
||||
class="q-ml-sm"
|
||||
>Copy On-Chain Address</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="statusDialog.show" position="top">
|
||||
<q-card v-if="statusDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<div>
|
||||
{% raw %}
|
||||
<b>Wallet: </b> {{ statusDialog.data.wallet }} <br />
|
||||
<b>Boltz Status: </b> {{ statusDialog.data.boltz }} <br />
|
||||
<b>Mempool Status: </b> {{ statusDialog.data.mempool }} <br />
|
||||
<b>Blockheight timeout: </b> {{ statusDialog.data.timeout_block_height
|
||||
}} <br />
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="refundSwap(statusDialog.data.swap_id)"
|
||||
v-if="!statusDialog.data.reverse"
|
||||
class="q-ml-sm"
|
||||
>Refund
|
||||
</q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="downloadRefundFile(statusDialog.data.swap_id)"
|
||||
v-if="!statusDialog.data.reverse"
|
||||
class="q-ml-sm"
|
||||
>Download refundfile</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="allStatusDialog.show" maximized position="top">
|
||||
<q-card v-if="allStatusDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<h5>pending swaps</h5>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="allStatusDialog.data.swaps"
|
||||
row-key="id"
|
||||
:columns="allStatusTable.columns"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="cached"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="refundSwap(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>refund swap</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="download"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="downloadRefundFile(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>dowload refund file</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
<h5>pending reverse swaps</h5>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="allStatusDialog.data.reverse_swaps"
|
||||
row-key="id"
|
||||
:columns="allStatusTable.columns"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="flip_to_front"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openMempool(props.row.swap_id)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-grey-8"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
>open tx on mempool.space</q-tooltip
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% include "boltz/_submarineSwapDialog.html" %} {% include
|
||||
"boltz/_reverseSubmarineSwapDialog.html" %} {% include
|
||||
"boltz/_autoReverseSwapDialog.html" %} {% include "boltz/_qrDialog.html" %} {%
|
||||
include "boltz/_statusDialog.html" %}
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
@@ -539,6 +27,7 @@
|
||||
boltzConfig: {},
|
||||
submarineSwaps: [],
|
||||
reverseSubmarineSwaps: [],
|
||||
autoReverseSubmarineSwaps: [],
|
||||
statuses: [],
|
||||
submarineSwapDialog: {
|
||||
show: false,
|
||||
@@ -550,6 +39,13 @@
|
||||
instant_settlement: true
|
||||
}
|
||||
},
|
||||
autoReverseSubmarineSwapDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
balance: 100,
|
||||
instant_settlement: true
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
@@ -558,40 +54,36 @@
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
allStatusDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
allStatusTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'swap_id',
|
||||
align: 'left',
|
||||
label: 'swap_id',
|
||||
label: 'Swap ID',
|
||||
field: 'swap_id'
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
align: 'left',
|
||||
label: 'status',
|
||||
label: 'Status',
|
||||
field: 'message'
|
||||
},
|
||||
{
|
||||
name: 'boltz',
|
||||
align: 'left',
|
||||
label: 'boltz',
|
||||
label: 'Boltz',
|
||||
field: 'boltz'
|
||||
},
|
||||
{
|
||||
name: 'mempool',
|
||||
align: 'left',
|
||||
label: 'mempool',
|
||||
label: 'Mempool',
|
||||
field: 'mempool'
|
||||
},
|
||||
{
|
||||
name: 'timeout_block_height',
|
||||
align: 'left',
|
||||
label: 'block height',
|
||||
label: 'Timeout block height',
|
||||
field: 'timeout_block_height'
|
||||
}
|
||||
],
|
||||
@@ -599,12 +91,60 @@
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
autoReverseSubmarineSwapTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'time',
|
||||
align: 'left',
|
||||
label: 'Time',
|
||||
field: 'time',
|
||||
sortable: true,
|
||||
format: function (val, row) {
|
||||
return new Date(val * 1000).toUTCString()
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'Wallet',
|
||||
field: data => {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: data.wallet
|
||||
})
|
||||
if (wallet) {
|
||||
return wallet.name
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'balance',
|
||||
align: 'left',
|
||||
label: 'Balance',
|
||||
field: 'balance'
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount',
|
||||
field: 'amount'
|
||||
},
|
||||
{
|
||||
name: 'onchain_address',
|
||||
align: 'left',
|
||||
label: 'Onchain address',
|
||||
field: 'onchain_address'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
reverseSubmarineSwapTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'time',
|
||||
align: 'left',
|
||||
label: 'time',
|
||||
label: 'Time',
|
||||
field: 'time',
|
||||
sortable: true,
|
||||
format: function (val, row) {
|
||||
@@ -614,7 +154,7 @@
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'wallet',
|
||||
label: 'Wallet',
|
||||
field: data => {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: data.wallet
|
||||
@@ -627,25 +167,25 @@
|
||||
{
|
||||
name: 'status',
|
||||
align: 'left',
|
||||
label: 'status',
|
||||
label: 'Status',
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
name: 'boltz_id',
|
||||
align: 'left',
|
||||
label: 'boltz id',
|
||||
label: 'Boltz ID',
|
||||
field: 'boltz_id'
|
||||
},
|
||||
{
|
||||
name: 'onchain_amount',
|
||||
align: 'left',
|
||||
label: 'onchain amount',
|
||||
label: 'Onchain amount',
|
||||
field: 'onchain_amount'
|
||||
},
|
||||
{
|
||||
name: 'timeout_block_height',
|
||||
align: 'left',
|
||||
label: 'timeout block height',
|
||||
label: 'Timeout block height',
|
||||
field: 'timeout_block_height'
|
||||
}
|
||||
],
|
||||
@@ -658,7 +198,7 @@
|
||||
{
|
||||
name: 'time',
|
||||
align: 'left',
|
||||
label: 'time',
|
||||
label: 'Time',
|
||||
field: 'time',
|
||||
sortable: true,
|
||||
format: function (val, row) {
|
||||
@@ -668,7 +208,7 @@
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'wallet',
|
||||
label: 'Wallet',
|
||||
field: data => {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: data.wallet
|
||||
@@ -681,25 +221,25 @@
|
||||
{
|
||||
name: 'status',
|
||||
align: 'left',
|
||||
label: 'status',
|
||||
label: 'Status',
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
name: 'boltz_id',
|
||||
align: 'left',
|
||||
label: 'boltz id',
|
||||
label: 'Boltz ID',
|
||||
field: 'boltz_id'
|
||||
},
|
||||
{
|
||||
name: 'expected_amount',
|
||||
align: 'left',
|
||||
label: 'expected amount',
|
||||
label: 'Expected amount',
|
||||
field: 'expected_amount'
|
||||
},
|
||||
{
|
||||
name: 'timeout_block_height',
|
||||
align: 'left',
|
||||
label: 'timeout block height',
|
||||
label: 'Timeout block height',
|
||||
field: 'timeout_block_height'
|
||||
}
|
||||
],
|
||||
@@ -711,11 +251,10 @@
|
||||
},
|
||||
methods: {
|
||||
getLimits() {
|
||||
const cfg = this.boltzConfig.data
|
||||
if (cfg) {
|
||||
if (this.boltzConfig) {
|
||||
return {
|
||||
min: cfg.limits.minimal,
|
||||
max: cfg.limits.maximal
|
||||
min: this.boltzConfig.minimal,
|
||||
max: this.boltzConfig.maximal
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -753,6 +292,19 @@
|
||||
data.amount > limits.max
|
||||
)
|
||||
},
|
||||
disableAutoReverseSubmarineSwapDialog() {
|
||||
const data = this.autoReverseSubmarineSwapDialog.data
|
||||
let limits = this.getLimits()
|
||||
return (
|
||||
data.onchain_address == null ||
|
||||
data.onchain_address.search(
|
||||
/^(bcrt1|bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
|
||||
) !== 0 ||
|
||||
data.wallet == null ||
|
||||
data.amount < limits.min ||
|
||||
data.amount > limits.max
|
||||
)
|
||||
},
|
||||
downloadRefundFile(swapId) {
|
||||
let swap = _.findWhere(this.submarineSwaps, {id: swapId})
|
||||
let json = {
|
||||
@@ -816,6 +368,7 @@
|
||||
swap_id: swap_id,
|
||||
wallet: res.data.wallet,
|
||||
boltz: res.data.boltz,
|
||||
status: res.data.status,
|
||||
mempool: res.data.mempool,
|
||||
timeout_block_height: res.data.timeout_block_height,
|
||||
date: new Date().toUTCString()
|
||||
@@ -847,12 +400,6 @@
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
resetAllStatusDialog() {
|
||||
this.allStatusDialog = {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
resetSubmarineSwapDialog() {
|
||||
this.submarineSwapDialog = {
|
||||
show: false,
|
||||
@@ -865,6 +412,12 @@
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
resetAutoReverseSubmarineSwapDialog() {
|
||||
this.autoReverseSubmarineSwapDialog = {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
sendReverseSubmarineSwapFormData() {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.reverseSubmarineSwapDialog.data.wallet
|
||||
@@ -872,6 +425,13 @@
|
||||
let data = this.reverseSubmarineSwapDialog.data
|
||||
this.createReverseSubmarineSwap(wallet, data)
|
||||
},
|
||||
sendAutoReverseSubmarineSwapFormData() {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.autoReverseSubmarineSwapDialog.data.wallet
|
||||
})
|
||||
let data = this.autoReverseSubmarineSwapDialog.data
|
||||
this.createAutoReverseSubmarineSwap(wallet, data)
|
||||
},
|
||||
sendSubmarineSwapFormData() {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.submarineSwapDialog.data.wallet
|
||||
@@ -891,6 +451,12 @@
|
||||
this.reverseSubmarineSwaps
|
||||
)
|
||||
},
|
||||
exportAutoReverseSubmarineSwapCSV() {
|
||||
LNbits.utils.exportCSV(
|
||||
this.autoReverseSubmarineSwapTable.columns,
|
||||
this.autoReverseSubmarineSwaps
|
||||
)
|
||||
},
|
||||
createSubmarineSwap(wallet, data) {
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -924,6 +490,40 @@
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
createAutoReverseSubmarineSwap(wallet, data) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/boltz/api/v1/swap/reverse/auto',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
data
|
||||
)
|
||||
.then(res => {
|
||||
this.autoReverseSubmarineSwaps.unshift(res.data)
|
||||
this.resetAutoReverseSubmarineSwapDialog()
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteAutoReverseSwap(swap_id) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/boltz/api/v1/swap/reverse/auto/' + swap_id,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(res => {
|
||||
let i = this.autoReverseSubmarineSwaps.findIndex(
|
||||
swap => swap.id === swap_id
|
||||
)
|
||||
this.autoReverseSubmarineSwaps.splice(i, 1)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getSubmarineSwap() {
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -952,6 +552,20 @@
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getAutoReverseSubmarineSwap() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/boltz/api/v1/swap/reverse/auto?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(response => {
|
||||
this.autoReverseSubmarineSwaps = response.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getMempool() {
|
||||
LNbits.api
|
||||
.request('GET', '/boltz/api/v1/swap/mempool')
|
||||
@@ -967,26 +581,7 @@
|
||||
LNbits.api
|
||||
.request('GET', '/boltz/api/v1/swap/boltz')
|
||||
.then(res => {
|
||||
this.boltzConfig = res
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('error', error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
checkSwaps() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/boltz/api/v1/swap/check',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(res => {
|
||||
this.allStatusDialog.data = {
|
||||
swaps: _.where(res.data, {reverse: false}),
|
||||
reverse_swaps: _.where(res.data, {reverse: true})
|
||||
}
|
||||
this.allStatusDialog.show = true
|
||||
this.boltzConfig = res.data
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('error', error)
|
||||
@@ -999,6 +594,7 @@
|
||||
this.getBoltzConfig()
|
||||
this.getSubmarineSwap()
|
||||
this.getReverseSubmarineSwap()
|
||||
this.getAutoReverseSubmarineSwap()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import asyncio
|
||||
import calendar
|
||||
import datetime
|
||||
from typing import Awaitable
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from boltz_client.boltz import BoltzClient, BoltzConfig
|
||||
|
||||
from lnbits.core.services import fee_reserve, get_wallet
|
||||
from lnbits.core.services import fee_reserve, get_wallet, pay_invoice
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .models import ReverseSubmarineSwap
|
||||
|
||||
|
||||
def create_boltz_client() -> BoltzClient:
|
||||
config = BoltzConfig(
|
||||
network=settings.boltz_network,
|
||||
api_url=settings.boltz_url,
|
||||
mempool_url=f"{settings.boltz_mempool_space_url}/api",
|
||||
mempool_ws_url=f"{settings.boltz_mempool_space_url_ws}/api/v1/ws",
|
||||
referral_id="lnbits",
|
||||
)
|
||||
return BoltzClient(config)
|
||||
|
||||
|
||||
async def check_balance(data) -> bool:
|
||||
@@ -23,22 +38,50 @@ def get_timestamp():
|
||||
return calendar.timegm(date.utctimetuple())
|
||||
|
||||
|
||||
def req_wrap(funcname, *args, **kwargs):
|
||||
try:
|
||||
async def execute_reverse_swap(client, swap: ReverseSubmarineSwap):
|
||||
# claim_task is watching onchain address for the lockup transaction to arrive / confirm
|
||||
# and if the lockup is there, claim the onchain revealing preimage for hold invoice
|
||||
claim_task = asyncio.create_task(
|
||||
client.claim_reverse_swap(
|
||||
privkey_wif=swap.claim_privkey,
|
||||
preimage_hex=swap.preimage,
|
||||
lockup_address=swap.lockup_address,
|
||||
receive_address=swap.onchain_address,
|
||||
redeem_script_hex=swap.redeem_script,
|
||||
)
|
||||
)
|
||||
# pay_task is paying the hold invoice which gets held until you reveal your preimage when claiming your onchain funds
|
||||
pay_task = pay_invoice_and_update_status(
|
||||
swap.id,
|
||||
claim_task,
|
||||
pay_invoice(
|
||||
wallet_id=swap.wallet,
|
||||
payment_request=swap.invoice,
|
||||
description=f"reverse swap for {swap.onchain_amount} sats on boltz.exchange",
|
||||
extra={"tag": "boltz", "swap_id": swap.id, "reverse": True},
|
||||
),
|
||||
)
|
||||
|
||||
# they need to run be concurrently, because else pay_task will lock the eventloop and claim_task will not be executed.
|
||||
# the lockup transaction can only happen after you pay the invoice, which cannot be redeemed immediatly -> hold invoice
|
||||
# after getting the lockup transaction, you can claim the onchain funds revealing the preimage for boltz to redeem the hold invoice
|
||||
asyncio.gather(claim_task, pay_task)
|
||||
|
||||
|
||||
def pay_invoice_and_update_status(
|
||||
swap_id: str, wstask: asyncio.Task, awaitable: Awaitable
|
||||
) -> asyncio.Task:
|
||||
async def _pay_invoice(awaitable):
|
||||
from .crud import update_swap_status
|
||||
|
||||
try:
|
||||
func = getattr(httpx, funcname)
|
||||
except AttributeError:
|
||||
logger.error('httpx function not found "%s"' % funcname)
|
||||
else:
|
||||
res = func(*args, timeout=30, **kwargs)
|
||||
res.raise_for_status()
|
||||
return res
|
||||
except httpx.RequestError as exc:
|
||||
msg = f"Unreachable: {exc.request.url!r}."
|
||||
logger.error(msg)
|
||||
raise
|
||||
except httpx.HTTPStatusError as exc:
|
||||
msg = f"HTTP Status Error: {exc.response.status_code} while requesting {exc.request.url!r}."
|
||||
logger.error(msg)
|
||||
logger.error(exc.response.json()["error"])
|
||||
raise
|
||||
awaited = await awaitable
|
||||
await update_swap_status(swap_id, "complete")
|
||||
return awaited
|
||||
except asyncio.exceptions.CancelledError:
|
||||
"""lnbits process was exited, do nothing and handle it in startup script"""
|
||||
except:
|
||||
wstask.cancel()
|
||||
await update_swap_status(swap_id, "failed")
|
||||
|
||||
return asyncio.create_task(_pay_invoice(awaitable))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import Payment, User
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import boltz_ext, boltz_renderer
|
||||
@@ -16,7 +15,6 @@ templates = Jinja2Templates(directory="templates")
|
||||
@boltz_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
root_url = urlparse(str(request.url)).netloc
|
||||
wallet_ids = [wallet.id for wallet in user.wallets]
|
||||
return boltz_renderer().TemplateResponse(
|
||||
"boltz/index.html",
|
||||
{"request": request, "user": user.dict(), "root_url": root_url},
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from fastapi import status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.param_functions import Body
|
||||
from fastapi.params import Depends, Query
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, Query, status
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.settings import settings
|
||||
|
||||
from . import boltz_ext
|
||||
from .boltz import (
|
||||
create_refund_tx,
|
||||
create_reverse_swap,
|
||||
create_swap,
|
||||
get_boltz_pairs,
|
||||
get_swap_status,
|
||||
)
|
||||
from .crud import (
|
||||
create_auto_reverse_submarine_swap,
|
||||
create_reverse_submarine_swap,
|
||||
create_submarine_swap,
|
||||
get_pending_reverse_submarine_swaps,
|
||||
get_pending_submarine_swaps,
|
||||
delete_auto_reverse_submarine_swap,
|
||||
get_auto_reverse_submarine_swap_by_wallet,
|
||||
get_auto_reverse_submarine_swaps,
|
||||
get_reverse_submarine_swap,
|
||||
get_reverse_submarine_swaps,
|
||||
get_submarine_swap,
|
||||
@@ -36,12 +25,14 @@ from .crud import (
|
||||
update_swap_status,
|
||||
)
|
||||
from .models import (
|
||||
AutoReverseSubmarineSwap,
|
||||
CreateAutoReverseSubmarineSwap,
|
||||
CreateReverseSubmarineSwap,
|
||||
CreateSubmarineSwap,
|
||||
ReverseSubmarineSwap,
|
||||
SubmarineSwap,
|
||||
)
|
||||
from .utils import check_balance
|
||||
from .utils import check_balance, create_boltz_client, execute_reverse_swap
|
||||
|
||||
|
||||
@boltz_ext.get(
|
||||
@@ -76,17 +67,8 @@ async def api_submarineswap(
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
|
||||
for swap in await get_pending_submarine_swaps(wallet_ids):
|
||||
swap_status = get_swap_status(swap)
|
||||
if swap_status.hit_timeout:
|
||||
if not swap_status.has_lockup:
|
||||
logger.warning(
|
||||
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
||||
)
|
||||
await update_swap_status(swap.id, "timeout")
|
||||
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
return [swap.dict() for swap in await get_submarine_swaps(wallet_ids)]
|
||||
|
||||
|
||||
@@ -109,35 +91,29 @@ async def api_submarineswap(
|
||||
},
|
||||
},
|
||||
)
|
||||
async def api_submarineswap_refund(
|
||||
swap_id: str,
|
||||
g: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
if swap_id == None:
|
||||
async def api_submarineswap_refund(swap_id: str):
|
||||
if not swap_id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="swap_id missing"
|
||||
)
|
||||
|
||||
swap = await get_submarine_swap(swap_id)
|
||||
if swap == None:
|
||||
if not swap:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
|
||||
)
|
||||
|
||||
if swap.status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="swap is not pending."
|
||||
)
|
||||
|
||||
try:
|
||||
await create_refund_tx(swap)
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
|
||||
client = create_boltz_client()
|
||||
await client.refund_swap(
|
||||
privkey_wif=swap.refund_privkey,
|
||||
lockup_address=swap.address,
|
||||
receive_address=swap.refund_address,
|
||||
redeem_script_hex=swap.redeem_script,
|
||||
timeout_block_height=swap.timeout_block_height,
|
||||
)
|
||||
|
||||
await update_swap_status(swap.id, "refunded")
|
||||
return swap
|
||||
@@ -153,37 +129,43 @@ async def api_submarineswap_refund(
|
||||
""",
|
||||
response_description="create swap",
|
||||
response_model=SubmarineSwap,
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
responses={
|
||||
405: {"description": "not allowed method, insufficient balance"},
|
||||
405: {
|
||||
"description": "auto reverse swap is active, a swap would immediatly be swapped out again."
|
||||
},
|
||||
500: {"description": "boltz error"},
|
||||
},
|
||||
)
|
||||
async def api_submarineswap_create(
|
||||
data: CreateSubmarineSwap,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
try:
|
||||
swap_data = await create_swap(data)
|
||||
except httpx.RequestError as exc:
|
||||
async def api_submarineswap_create(data: CreateSubmarineSwap):
|
||||
|
||||
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
|
||||
if auto_swap:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||
detail="auto reverse swap is active, a swap would immediatly be swapped out again.",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(
|
||||
status_code=exc.response.status_code, detail=exc.response.json()["error"]
|
||||
)
|
||||
swap = await create_submarine_swap(swap_data)
|
||||
return swap.dict()
|
||||
|
||||
client = create_boltz_client()
|
||||
swap_id = urlsafe_short_hash()
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=data.wallet,
|
||||
amount=data.amount,
|
||||
memo=f"swap of {data.amount} sats on boltz.exchange",
|
||||
extra={"tag": "boltz", "swap_id": swap_id},
|
||||
)
|
||||
refund_privkey_wif, swap = client.create_swap(payment_request)
|
||||
new_swap = await create_submarine_swap(
|
||||
data, swap, swap_id, refund_privkey_wif, payment_hash
|
||||
)
|
||||
return new_swap.dict() if new_swap else None
|
||||
|
||||
|
||||
# REVERSE SWAP
|
||||
@boltz_ext.get(
|
||||
"/api/v1/swap/reverse",
|
||||
name=f"boltz.get /swap/reverse",
|
||||
summary="get a list of reverse swaps a swap",
|
||||
summary="get a list of reverse swaps",
|
||||
description="""
|
||||
This endpoint gets a list of reverse swaps.
|
||||
""",
|
||||
@@ -192,13 +174,14 @@ async def api_submarineswap_create(
|
||||
response_model=List[ReverseSubmarineSwap],
|
||||
)
|
||||
async def api_reverse_submarineswap(
|
||||
g: WalletTypeInfo = Depends(get_key_type), # type:ignore
|
||||
g: WalletTypeInfo = Depends(get_key_type),
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
return [swap.dict() for swap in await get_reverse_submarine_swaps(wallet_ids)]
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
return [swap for swap in await get_reverse_submarine_swaps(wallet_ids)]
|
||||
|
||||
|
||||
@boltz_ext.post(
|
||||
@@ -211,6 +194,7 @@ async def api_reverse_submarineswap(
|
||||
""",
|
||||
response_description="create reverse swap",
|
||||
response_model=ReverseSubmarineSwap,
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
responses={
|
||||
405: {"description": "not allowed method, insufficient balance"},
|
||||
500: {"description": "boltz error"},
|
||||
@@ -218,30 +202,88 @@ async def api_reverse_submarineswap(
|
||||
)
|
||||
async def api_reverse_submarineswap_create(
|
||||
data: CreateReverseSubmarineSwap,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
) -> ReverseSubmarineSwap:
|
||||
|
||||
if not await check_balance(data):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Insufficient balance."
|
||||
)
|
||||
client = create_boltz_client()
|
||||
claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(
|
||||
amount=data.amount
|
||||
)
|
||||
new_swap = await create_reverse_submarine_swap(
|
||||
data, claim_privkey_wif, preimage_hex, swap
|
||||
)
|
||||
await execute_reverse_swap(client, new_swap)
|
||||
return new_swap
|
||||
|
||||
try:
|
||||
swap_data, task = await create_reverse_swap(data)
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(
|
||||
status_code=exc.response.status_code, detail=exc.response.json()["error"]
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail=str(exc))
|
||||
|
||||
swap = await create_reverse_submarine_swap(swap_data)
|
||||
return swap.dict()
|
||||
@boltz_ext.get(
|
||||
"/api/v1/swap/reverse/auto",
|
||||
name=f"boltz.get /swap/reverse/auto",
|
||||
summary="get a list of auto reverse swaps",
|
||||
description="""
|
||||
This endpoint gets a list of auto reverse swaps.
|
||||
""",
|
||||
response_description="list of auto reverse swaps",
|
||||
dependencies=[Depends(get_key_type)],
|
||||
response_model=List[AutoReverseSubmarineSwap],
|
||||
)
|
||||
async def api_auto_reverse_submarineswap(
|
||||
g: WalletTypeInfo = Depends(get_key_type),
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
if all_wallets:
|
||||
user = await get_user(g.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
return [swap.dict() for swap in await get_auto_reverse_submarine_swaps(wallet_ids)]
|
||||
|
||||
|
||||
@boltz_ext.post(
|
||||
"/api/v1/swap/reverse/auto",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
name=f"boltz.post /swap/reverse/auto",
|
||||
summary="create a auto reverse submarine swap",
|
||||
description="""
|
||||
This endpoint creates a auto reverse submarine swap
|
||||
""",
|
||||
response_description="create auto reverse swap",
|
||||
response_model=AutoReverseSubmarineSwap,
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
responses={
|
||||
405: {
|
||||
"description": "auto reverse swap is active, only 1 swap per wallet possible."
|
||||
},
|
||||
},
|
||||
)
|
||||
async def api_auto_reverse_submarineswap_create(data: CreateAutoReverseSubmarineSwap):
|
||||
|
||||
auto_swap = await get_auto_reverse_submarine_swap_by_wallet(data.wallet)
|
||||
if auto_swap:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||
detail="auto reverse swap is active, only 1 swap per wallet possible.",
|
||||
)
|
||||
|
||||
swap = await create_auto_reverse_submarine_swap(data)
|
||||
return swap.dict() if swap else None
|
||||
|
||||
|
||||
@boltz_ext.delete(
|
||||
"/api/v1/swap/reverse/auto/{swap_id}",
|
||||
name=f"boltz.delete /swap/reverse/auto",
|
||||
summary="delete a auto reverse submarine swap",
|
||||
description="""
|
||||
This endpoint deletes a auto reverse submarine swap
|
||||
""",
|
||||
response_description="delete auto reverse swap",
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
)
|
||||
async def api_auto_reverse_submarineswap_delete(swap_id: str):
|
||||
await delete_auto_reverse_submarine_swap(swap_id)
|
||||
return "OK"
|
||||
|
||||
|
||||
@boltz_ext.post(
|
||||
@@ -252,65 +294,22 @@ async def api_reverse_submarineswap_create(
|
||||
This endpoint attempts to get the status of the swap.
|
||||
""",
|
||||
response_description="status of swap json",
|
||||
dependencies=[Depends(require_admin_key)],
|
||||
responses={
|
||||
404: {"description": "when swap_id is not found"},
|
||||
},
|
||||
)
|
||||
async def api_swap_status(
|
||||
swap_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
|
||||
):
|
||||
async def api_swap_status(swap_id: str):
|
||||
swap = await get_submarine_swap(swap_id) or await get_reverse_submarine_swap(
|
||||
swap_id
|
||||
)
|
||||
if swap == None:
|
||||
if not swap:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="swap does not exist."
|
||||
)
|
||||
try:
|
||||
status = get_swap_status(swap)
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
)
|
||||
return status
|
||||
|
||||
|
||||
@boltz_ext.post(
|
||||
"/api/v1/swap/check",
|
||||
name=f"boltz.swap_check",
|
||||
summary="list all pending swaps",
|
||||
description="""
|
||||
This endpoint gives you 2 lists of pending swaps and reverse swaps.
|
||||
""",
|
||||
response_description="list of pending swaps",
|
||||
)
|
||||
async def api_check_swaps(
|
||||
g: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
all_wallets: bool = Query(False),
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||
status = []
|
||||
try:
|
||||
for swap in await get_pending_submarine_swaps(wallet_ids):
|
||||
status.append(get_swap_status(swap))
|
||||
for reverseswap in await get_pending_reverse_submarine_swaps(wallet_ids):
|
||||
status.append(get_swap_status(reverseswap))
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
)
|
||||
client = create_boltz_client()
|
||||
status = client.swap_status(swap.boltz_id)
|
||||
return status
|
||||
|
||||
|
||||
@@ -325,14 +324,5 @@ async def api_check_swaps(
|
||||
response_model=dict,
|
||||
)
|
||||
async def api_boltz_config():
|
||||
try:
|
||||
res = get_boltz_pairs()
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Unreachable: {exc.request.url!r}.",
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
return res["pairs"]["BTC/BTC"]
|
||||
client = create_boltz_client()
|
||||
return {"minimal": client.limit_minimal, "maximal": client.limit_maximal}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
from environs import Env # type: ignore
|
||||
from environs import Env
|
||||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Cashu",
|
||||
"short_description": "Ecash mint and wallet",
|
||||
"icon": "account_balance",
|
||||
"tile": "/cashu/static/image/cashu.png",
|
||||
"contributors": ["calle", "vlad", "arcbtc"],
|
||||
"hidden": false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from cashu.core.base import MintKeyset
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -28,6 +28,7 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra and not payment.extra.get("tag") == "cashu":
|
||||
if payment.extra.get("tag") != "cashu":
|
||||
return
|
||||
|
||||
return
|
||||
|
||||
@@ -4,9 +4,24 @@
|
||||
<p>Create Cashu ecash mints and wallets.</p>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
||||
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
||||
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/arcbtc"
|
||||
target="_blank"
|
||||
>arcbtc</a
|
||||
>,
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/motorina0"
|
||||
target="_blank"
|
||||
>vlad</a
|
||||
>,
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/calle"
|
||||
target="_blank"
|
||||
>calle</a
|
||||
>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
<q-card class="q-pa-lg q-mb-xl">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<q-icon
|
||||
name="account_balance"
|
||||
class="text-grey"
|
||||
style="font-size: 10rem"
|
||||
></q-icon>
|
||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||
<q-img
|
||||
src="/cashu/static/image/cashu.png"
|
||||
spinner-color="white"
|
||||
style="max-width: 20%"
|
||||
></q-img>
|
||||
<h4 class="q-mt-sm q-mb-md">{{ mint_name }}</h4>
|
||||
<!-- <a class="text-secondary">Mint URL: {{testfield}} </a> <br /> -->
|
||||
<a
|
||||
class="text-secondary"
|
||||
class="q-my-xl text-white"
|
||||
style="font-size: 1.5rem"
|
||||
href="../wallet?mint_id={{ mint_id }}"
|
||||
>Open wallet</a
|
||||
>click to open wallet</a
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
@@ -24,7 +26,11 @@
|
||||
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://cashu.space/" style="color: white" target="”_blank”"
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://cashu.space/"
|
||||
style="color: white"
|
||||
target="”_blank”"
|
||||
>Cashu</a
|
||||
>
|
||||
mint. Cashu is an ecash system for Bitcoin.
|
||||
@@ -53,24 +59,34 @@
|
||||
</p>
|
||||
<p>
|
||||
<strong>This service is in BETA</strong> <br />
|
||||
We hold no responsibility for people losing access to funds. Use at
|
||||
your own risk!
|
||||
Cashu is still experimental and in active development. There are
|
||||
likely bugs in this implementation so please use this with caution. We
|
||||
hold no responsibility for people losing access to funds. Use at your
|
||||
own risk!
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
testfield: 'asd',
|
||||
mintURL: {
|
||||
location: window.location,
|
||||
base_url: location.protocol + '//' + location.host + location.pathname
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -159,7 +159,7 @@ page_container %}
|
||||
size="lg"
|
||||
color="secondary"
|
||||
class="q-mr-md cursor-pointer"
|
||||
@click="recheckInvoice(props.row.hash)"
|
||||
@click="checkInvoice(props.row.hash)"
|
||||
>
|
||||
Check
|
||||
</q-badge>
|
||||
@@ -616,10 +616,10 @@ page_container %}
|
||||
></q-input>
|
||||
</div>
|
||||
<div v-else class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + invoiceData.bolt11">
|
||||
<a class="text-secondary" :href="'lightning:' + invoiceData.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="invoiceData.bolt11"
|
||||
:value="'lightning:' + invoiceData.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
>
|
||||
@@ -681,7 +681,7 @@ page_container %}
|
||||
</div>
|
||||
<div v-else class="text-center q-mb-lg">
|
||||
<div class="text-center q-mb-lg">
|
||||
<!-- <a :href="'cashu:' + sendData.tokensBase64"> -->
|
||||
<!-- <a class="text-secondary" :href="'cashu:' + sendData.tokensBase64"> -->
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="disclaimerDialog.base_url + '?mint_id=' + mintId + '&recv_token=' + sendData.tokensBase64"
|
||||
@@ -1479,15 +1479,15 @@ page_container %}
|
||||
},
|
||||
|
||||
constructOutputs: async function (amounts, secrets) {
|
||||
const blindedMessages = []
|
||||
const outputs = []
|
||||
const rs = []
|
||||
for (let i = 0; i < amounts.length; i++) {
|
||||
const {B_, r} = await step1Alice(secrets[i])
|
||||
blindedMessages.push({amount: amounts[i], B_: B_})
|
||||
outputs.push({amount: amounts[i], B_: B_})
|
||||
rs.push(r)
|
||||
}
|
||||
return {
|
||||
blindedMessages,
|
||||
outputs,
|
||||
rs
|
||||
}
|
||||
},
|
||||
@@ -1528,57 +1528,17 @@ page_container %}
|
||||
return proofs.reduce((s, t) => (s += t.amount), 0)
|
||||
},
|
||||
|
||||
deleteProofs: function (proofs) {
|
||||
// delete proofs from this.proofs
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||
this.storeProofs()
|
||||
return this.proofs
|
||||
},
|
||||
|
||||
//////////// API ///////////
|
||||
clearAllWorkers: function () {
|
||||
if (this.invoiceCheckListener) {
|
||||
clearInterval(this.invoiceCheckListener)
|
||||
}
|
||||
if (this.tokensCheckSpendableListener) {
|
||||
clearInterval(this.tokensCheckSpendableListener)
|
||||
}
|
||||
},
|
||||
invoiceCheckWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.invoiceCheckListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
|
||||
// exit loop after 2m
|
||||
if (nInterval > 40) {
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||
console.log(this.invoiceData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
await this.recheckInvoice(this.invoiceData.hash, false)
|
||||
|
||||
// only without error (invoice paid) will we reach here
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
this.invoiceData.bolt11 = ''
|
||||
this.showInvoiceDetails = false
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Payment received',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
// MINT
|
||||
|
||||
requestMintButton: async function () {
|
||||
await this.requestMint()
|
||||
@@ -1586,8 +1546,12 @@ page_container %}
|
||||
await this.invoiceCheckWorker()
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
requestMint: async function () {
|
||||
// gets an invoice from the mint to get new tokens
|
||||
/*
|
||||
gets an invoice from the mint to get new tokens
|
||||
*/
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
@@ -1611,24 +1575,32 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
||||
/*
|
||||
asks the mint to check whether the invoice with payment_hash has been paid
|
||||
and requests signing of the attached outputs.
|
||||
*/
|
||||
console.log('### promises', payment_hash)
|
||||
try {
|
||||
let secrets = await this.generateSecrets(amounts)
|
||||
let {blindedMessages, rs} = await this.constructOutputs(
|
||||
amounts,
|
||||
secrets
|
||||
)
|
||||
let {outputs, rs} = await this.constructOutputs(amounts, secrets)
|
||||
const promises = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/mint?payment_hash=${payment_hash}`,
|
||||
'',
|
||||
{
|
||||
blinded_messages: blindedMessages
|
||||
outputs
|
||||
}
|
||||
)
|
||||
console.log('### promises data', promises.data)
|
||||
let proofs = await this.constructProofs(promises.data, secrets, rs)
|
||||
console.log('### promises data', promises.data.promises)
|
||||
let proofs = await this.constructProofs(
|
||||
promises.data.promises,
|
||||
secrets,
|
||||
rs
|
||||
)
|
||||
return proofs
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -1647,7 +1619,19 @@ page_container %}
|
||||
}
|
||||
this.proofs = this.proofs.concat(proofs)
|
||||
this.storeProofs()
|
||||
|
||||
// update UI
|
||||
await this.setInvoicePaid(payment_hash)
|
||||
tokensBase64 = btoa(JSON.stringify(proofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: amount,
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
|
||||
return proofs
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -1657,62 +1641,20 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||
// splits proofs so the user can keep firstProofs, send scndProofs
|
||||
try {
|
||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||
if (this.sumProofs(spendableProofs) < amount) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Balance too low',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw Error('balance too low.')
|
||||
}
|
||||
let {fristProofs, scndProofs} = await this.split(
|
||||
spendableProofs,
|
||||
amount
|
||||
)
|
||||
|
||||
// set scndProofs in this.proofs as reserved
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
for (let i = 0; i < this.proofs.length; i++) {
|
||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||
this.proofs[i].reserved = true
|
||||
}
|
||||
}
|
||||
if (invlalidate) {
|
||||
// delete tokens from db
|
||||
this.proofs = fristProofs
|
||||
// add new fristProofs, scndProofs to this.proofs
|
||||
this.storeProofs()
|
||||
}
|
||||
|
||||
return {fristProofs, scndProofs}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// SPLIT
|
||||
|
||||
split: async function (proofs, amount) {
|
||||
/*
|
||||
supplies proofs and requests a split from the mint of these
|
||||
proofs at a specific amount
|
||||
*/
|
||||
try {
|
||||
if (proofs.length == 0) {
|
||||
throw new Error('no proofs provided.')
|
||||
}
|
||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||
// delete proofs from this.proofs
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||
this.deleteProofs(proofs)
|
||||
// add new fristProofs, scndProofs to this.proofs
|
||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||
this.storeProofs()
|
||||
@@ -1723,6 +1665,9 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /split
|
||||
|
||||
splitApi: async function (proofs, amount) {
|
||||
try {
|
||||
const total = this.sumProofs(proofs)
|
||||
@@ -1738,16 +1683,11 @@ page_container %}
|
||||
'number of secrets does not match number of outputs.'
|
||||
)
|
||||
}
|
||||
let {blindedMessages, rs} = await this.constructOutputs(
|
||||
amounts,
|
||||
secrets
|
||||
)
|
||||
let {outputs, rs} = await this.constructOutputs(amounts, secrets)
|
||||
const payload = {
|
||||
amount,
|
||||
proofs,
|
||||
outputs: {
|
||||
blinded_messages: blindedMessages
|
||||
}
|
||||
outputs
|
||||
}
|
||||
|
||||
console.log('payload', JSON.stringify(payload))
|
||||
@@ -1782,7 +1722,62 @@ page_container %}
|
||||
}
|
||||
},
|
||||
|
||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||
/*
|
||||
splits proofs so the user can keep firstProofs, send scndProofs.
|
||||
then sets scndProofs as reserved.
|
||||
|
||||
if invalidate, scndProofs (the one to send) are invalidated
|
||||
*/
|
||||
try {
|
||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||
if (this.sumProofs(spendableProofs) < amount) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Balance too low',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw Error('balance too low.')
|
||||
}
|
||||
|
||||
// call /split
|
||||
|
||||
let {fristProofs, scndProofs} = await this.split(
|
||||
spendableProofs,
|
||||
amount
|
||||
)
|
||||
// set scndProofs in this.proofs as reserved
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
for (let i = 0; i < this.proofs.length; i++) {
|
||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||
this.proofs[i].reserved = true
|
||||
}
|
||||
}
|
||||
if (invlalidate) {
|
||||
// delete scndProofs from db
|
||||
this.deleteProofs(scndProofs)
|
||||
}
|
||||
|
||||
return {fristProofs, scndProofs}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
redeem: async function () {
|
||||
/*
|
||||
uses split to receive new tokens.
|
||||
*/
|
||||
this.showReceiveTokens = false
|
||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||
try {
|
||||
@@ -1793,6 +1788,9 @@ page_container %}
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||
|
||||
// update UI
|
||||
|
||||
// HACK: we need to do this so the balance updates
|
||||
this.proofs = this.proofs.concat([])
|
||||
|
||||
@@ -1827,13 +1825,18 @@ page_container %}
|
||||
},
|
||||
|
||||
sendTokens: async function () {
|
||||
/*
|
||||
calls splitToSend, displays token and kicks off the spendableWorker
|
||||
*/
|
||||
try {
|
||||
// keep firstProofs, send scndProofs
|
||||
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||
this.proofs,
|
||||
this.sendData.amount,
|
||||
true
|
||||
)
|
||||
|
||||
// update UI
|
||||
this.sendData.tokens = scndProofs
|
||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||
this.sendData.tokensBase64 = btoa(
|
||||
@@ -1846,33 +1849,19 @@ page_container %}
|
||||
date: currentDateStr(),
|
||||
token: this.sendData.tokensBase64
|
||||
})
|
||||
|
||||
// store "pending" outgoing tokens in history table
|
||||
this.storehistoryTokens()
|
||||
|
||||
this.checkTokenSpendableWorker()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
checkFees: async function (payment_request) {
|
||||
const payload = {
|
||||
pr: payment_request
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
console.log('#### checkFees', payment_request, data.fee)
|
||||
return data.fee
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /melt
|
||||
|
||||
melt: async function () {
|
||||
// todo: get fees from server and add to inputs
|
||||
this.payInvoiceData.blocking = true
|
||||
@@ -1888,10 +1877,7 @@ page_container %}
|
||||
'amount with fees',
|
||||
amount
|
||||
)
|
||||
// if (amount > balance()) {
|
||||
// LNbits.utils.notifyApiError('Balance too low')
|
||||
// return
|
||||
// }
|
||||
|
||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||
this.proofs,
|
||||
amount
|
||||
@@ -1924,8 +1910,20 @@ page_container %}
|
||||
]
|
||||
})
|
||||
// delete spent tokens from db
|
||||
this.proofs = fristProofs
|
||||
this.storeProofs()
|
||||
this.deleteProofs(scndProofs)
|
||||
|
||||
// update UI
|
||||
|
||||
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: -amount,
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
|
||||
console.log({
|
||||
amount: -amount,
|
||||
bolt11: this.payInvoiceData.data.request,
|
||||
@@ -1953,13 +1951,95 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /check
|
||||
|
||||
checkProofsSpendable: async function (proofs, update_history = false) {
|
||||
/*
|
||||
checks with the mint whether an array of proofs is still
|
||||
spendable or already invalidated
|
||||
*/
|
||||
const payload = {
|
||||
proofs: proofs.flat()
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/check`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
|
||||
// delete proofs from database if it is spent
|
||||
let spentProofs = proofs.filter((p, pidx) => !data[pidx])
|
||||
if (spentProofs.length) {
|
||||
this.deleteProofs(spentProofs)
|
||||
|
||||
// update UI
|
||||
if (update_history) {
|
||||
tokensBase64 = btoa(JSON.stringify(spentProofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: -this.sumProofs(spentProofs),
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /checkfees
|
||||
checkFees: async function (payment_request) {
|
||||
const payload = {
|
||||
pr: payment_request
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
console.log('#### checkFees', payment_request, data.fee)
|
||||
return data.fee
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /keys
|
||||
|
||||
fetchMintKeys: async function () {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/cashu/api/v1/${this.mintId}/keys`
|
||||
)
|
||||
this.keys = data
|
||||
localStorage.setItem(
|
||||
this.mintKey(this.mintId, 'keys'),
|
||||
JSON.stringify(data)
|
||||
)
|
||||
},
|
||||
setInvoicePaid: async function (payment_hash) {
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
invoice.status = 'paid'
|
||||
this.storeinvoicesCashu()
|
||||
},
|
||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### recheckInvoice.hash', payment_hash)
|
||||
checkInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### checkInvoice.hash', payment_hash)
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
try {
|
||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||
@@ -1969,15 +2049,15 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
recheckPendingInvoices: async function () {
|
||||
checkPendingInvoices: async function () {
|
||||
for (const invoice of this.invoicesCashu) {
|
||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
||||
this.recheckInvoice(invoice.hash, false)
|
||||
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||
this.checkInvoice(invoice.hash, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recheckPendingTokens: async function () {
|
||||
checkPendingTokens: async function () {
|
||||
for (const token of this.historyTokens) {
|
||||
if (token.status === 'pending' && token.amount < 0) {
|
||||
this.checkTokenSpendable(token.token, false)
|
||||
@@ -1990,6 +2070,126 @@ page_container %}
|
||||
this.storehistoryTokens()
|
||||
},
|
||||
|
||||
checkTokenSpendable: async function (token, verbose = true) {
|
||||
/*
|
||||
checks whether a base64-encoded token (from the history table) has been spent already.
|
||||
if it is spent, the appropraite entry in the history table is set to paid.
|
||||
*/
|
||||
const tokenJson = atob(token)
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
let data = await this.checkProofsSpendable(proofs)
|
||||
|
||||
// iterate through response of form {0: true, 1: false, ...}
|
||||
let paid = false
|
||||
for (const [key, spendable] of Object.entries(data)) {
|
||||
if (!spendable) {
|
||||
this.setTokenPaid(token)
|
||||
paid = true
|
||||
}
|
||||
}
|
||||
if (paid) {
|
||||
console.log('### token paid')
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Token paid',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.log('### token not paid yet')
|
||||
if (verbose) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
color: 'grey',
|
||||
message: 'Token still pending',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
this.sendData.tokens = token
|
||||
}
|
||||
return paid
|
||||
},
|
||||
|
||||
findTokenForAmount: function (amount) {
|
||||
for (const token of this.proofs) {
|
||||
const index = token.promises?.findIndex(p => p.amount === amount)
|
||||
if (index >= 0) {
|
||||
return {
|
||||
promise: token.promises[index],
|
||||
secret: token.secrets[index],
|
||||
r: token.rs[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
////////////// WORKERS //////////////
|
||||
|
||||
clearAllWorkers: function () {
|
||||
if (this.invoiceCheckListener) {
|
||||
clearInterval(this.invoiceCheckListener)
|
||||
}
|
||||
if (this.tokensCheckSpendableListener) {
|
||||
clearInterval(this.tokensCheckSpendableListener)
|
||||
}
|
||||
},
|
||||
invoiceCheckWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.invoiceCheckListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
|
||||
// exit loop after 2m
|
||||
if (nInterval > 40) {
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||
console.log(this.invoiceData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
await this.checkInvoice(this.invoiceData.hash, false)
|
||||
|
||||
// only without error (invoice paid) will we reach here
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
this.invoiceData.bolt11 = ''
|
||||
this.showInvoiceDetails = false
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Payment received',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
checkTokenSpendableWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
@@ -2021,158 +2221,11 @@ page_container %}
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
checkTokenSpendable: async function (token, verbose = true) {
|
||||
const tokenJson = atob(token)
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
const payload = {
|
||||
proofs: proofs.flat()
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/check`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
// iterate through response of form {0: true, 1: false, ...}
|
||||
let paid = false
|
||||
for (const [key, spendable] of Object.entries(data)) {
|
||||
if (!spendable) {
|
||||
this.setTokenPaid(token)
|
||||
paid = true
|
||||
}
|
||||
}
|
||||
if (paid) {
|
||||
console.log('### token paid')
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Token paid',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.log('### token not paid yet')
|
||||
if (verbose) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
color: 'grey',
|
||||
message: 'Token still pending',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
this.sendData.tokens = token
|
||||
}
|
||||
return paid
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
fetchMintKeys: async function () {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/cashu/api/v1/${this.mintId}/keys`
|
||||
)
|
||||
this.keys = data
|
||||
localStorage.setItem(
|
||||
this.mintKey(this.mintId, 'keys'),
|
||||
JSON.stringify(data)
|
||||
)
|
||||
},
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
findTokenForAmount: function (amount) {
|
||||
for (const token of this.proofs) {
|
||||
const index = token.promises?.findIndex(p => p.amount === amount)
|
||||
if (index >= 0) {
|
||||
return {
|
||||
promise: token.promises[index],
|
||||
secret: token.secrets[index],
|
||||
r: token.rs[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
checkInvoice: function () {
|
||||
console.log('#### checkInvoice')
|
||||
try {
|
||||
const invoice = decode(this.payInvoiceData.data.request)
|
||||
|
||||
const cleanInvoice = {
|
||||
msat: invoice.human_readable_part.amount,
|
||||
sat: invoice.human_readable_part.amount / 1000,
|
||||
fsat: LNbits.utils.formatSat(
|
||||
invoice.human_readable_part.amount / 1000
|
||||
)
|
||||
}
|
||||
|
||||
_.each(invoice.data.tags, tag => {
|
||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
if (tag.description === 'payment_hash') {
|
||||
cleanInvoice.hash = tag.value
|
||||
} else if (tag.description === 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description === 'expiry') {
|
||||
var expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
expireDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
}
|
||||
|
||||
this.payInvoiceData.invoice = cleanInvoice
|
||||
})
|
||||
|
||||
console.log(
|
||||
'#### this.payInvoiceData.invoice',
|
||||
this.payInvoiceData.invoice
|
||||
)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Could not decode invoice',
|
||||
caption: error + '',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
////////////// STORAGE /////////////
|
||||
|
||||
getLocalstorageToFile: async function () {
|
||||
@@ -2335,8 +2388,9 @@ page_container %}
|
||||
console.log('#### this.mintId', this.mintId)
|
||||
console.log('#### this.mintName', this.mintName)
|
||||
|
||||
this.recheckPendingInvoices()
|
||||
this.recheckPendingTokens()
|
||||
this.checkProofsSpendable(this.proofs, true)
|
||||
this.checkPendingInvoices()
|
||||
this.checkPendingTokens()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
@@ -18,7 +17,7 @@ templates = Jinja2Templates(directory="templates")
|
||||
@cashu_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists), # type: ignore
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
return cashu_renderer().TemplateResponse(
|
||||
"cashu/index.html", {"request": request, "user": user.dict()}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import json
|
||||
import math
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import httpx
|
||||
|
||||
# -------- cashu imports
|
||||
from cashu.core.base import (
|
||||
BlindedSignature,
|
||||
@@ -15,16 +12,13 @@ from cashu.core.base import (
|
||||
GetMintResponse,
|
||||
Invoice,
|
||||
MeltRequest,
|
||||
MintRequest,
|
||||
PostMintRequest,
|
||||
PostMintResponse,
|
||||
PostSplitResponse,
|
||||
Proof,
|
||||
SplitRequest,
|
||||
)
|
||||
from fastapi import Query
|
||||
from fastapi.params import Depends
|
||||
from lnurl import decode as decode_lnurl
|
||||
from fastapi import Depends, Query
|
||||
from loguru import logger
|
||||
from secp256k1 import PublicKey
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits import bolt11
|
||||
@@ -35,7 +29,6 @@ from lnbits.core.services import (
|
||||
fee_reserve,
|
||||
pay_invoice,
|
||||
)
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
@@ -46,9 +39,16 @@ from .models import Cashu
|
||||
|
||||
# --------- extension imports
|
||||
|
||||
# WARNING: Do not set this to False in production! This will create
|
||||
# tokens for free otherwise. This is for testing purposes only!
|
||||
|
||||
LIGHTNING = True
|
||||
|
||||
if not LIGHTNING:
|
||||
logger.warning(
|
||||
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||
)
|
||||
|
||||
########################################
|
||||
############### LNBITS MINTS ###########
|
||||
########################################
|
||||
@@ -56,7 +56,7 @@ LIGHTNING = True
|
||||
|
||||
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
|
||||
async def api_cashus(
|
||||
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
"""
|
||||
Get all mints of this wallet.
|
||||
@@ -73,7 +73,7 @@ async def api_cashus(
|
||||
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
|
||||
async def api_cashu_create(
|
||||
data: Cashu,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
"""
|
||||
Create a new mint for this wallet.
|
||||
@@ -91,7 +91,7 @@ async def api_cashu_create(
|
||||
|
||||
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
|
||||
async def api_cashu_delete(
|
||||
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
|
||||
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
"""
|
||||
Delete an existing cashu mint.
|
||||
@@ -130,6 +130,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
|
||||
async def keyset_keys(
|
||||
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
||||
) -> dict[int, str]:
|
||||
"""
|
||||
Get the public keys of the mint of a specificy keyset id.
|
||||
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||
normal base64 before it can be processed.
|
||||
"""
|
||||
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||
keyset = ledger.get_keyset(keyset_id=id)
|
||||
return keyset
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||
"""Get the public keys of the mint"""
|
||||
@@ -182,11 +204,11 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||
async def mint_coins(
|
||||
data: MintRequest,
|
||||
async def mint(
|
||||
data: PostMintRequest,
|
||||
cashu_id: str = Query(None),
|
||||
payment_hash: str = Query(None),
|
||||
) -> List[BlindedSignature]:
|
||||
) -> PostMintResponse:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
Call this endpoint after `GET /mint`.
|
||||
@@ -197,6 +219,8 @@ async def mint_coins(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
|
||||
if LIGHTNING:
|
||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash
|
||||
@@ -206,42 +230,51 @@ async def mint_coins(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Mint does not know this invoice.",
|
||||
)
|
||||
if invoice.issued == True:
|
||||
if invoice.issued:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail="Tokens already issued for this invoice.",
|
||||
)
|
||||
|
||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||
if total_requested > invoice.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||
)
|
||||
|
||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
||||
|
||||
if LIGHTNING and status.paid != True:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||
)
|
||||
try:
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
assert len(promises), HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
||||
)
|
||||
# set this invoice as issued
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash, issued=True
|
||||
)
|
||||
|
||||
return promises
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
status: PaymentStatus = await check_transaction_status(
|
||||
cashu.wallet, payment_hash
|
||||
)
|
||||
|
||||
try:
|
||||
total_requested = sum([bm.amount for bm in data.outputs])
|
||||
if total_requested > invoice.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||
)
|
||||
|
||||
if not status.paid:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||
)
|
||||
|
||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
||||
return PostMintResponse(promises=promises)
|
||||
except (Exception, HTTPException) as e:
|
||||
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||
# unset issued flag because something went wrong
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash, issued=False
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=getattr(e, "status_code")
|
||||
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=str(e) or getattr(e, "detail"),
|
||||
)
|
||||
else:
|
||||
# only used for testing when LIGHTNING=false
|
||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
||||
return PostMintResponse(promises=promises)
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||
@@ -285,28 +318,38 @@ async def melt_coins(
|
||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||
)
|
||||
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
||||
await pay_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
payment_request=invoice,
|
||||
description=f"Pay cashu invoice",
|
||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||
)
|
||||
status: PaymentStatus = await check_transaction_status(
|
||||
cashu.wallet, invoice_obj.payment_hash
|
||||
)
|
||||
if status.paid == True:
|
||||
logger.debug("Cashu: Payment successful, invalidating proofs")
|
||||
await ledger._invalidate_proofs(proofs)
|
||||
try:
|
||||
await pay_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
payment_request=invoice,
|
||||
description=f"Pay cashu invoice",
|
||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||
raise e
|
||||
finally:
|
||||
logger.debug(
|
||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||
)
|
||||
status: PaymentStatus = await check_transaction_status(
|
||||
cashu.wallet, invoice_obj.payment_hash
|
||||
)
|
||||
if status.paid == True:
|
||||
logger.debug(
|
||||
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
||||
)
|
||||
await ledger._invalidate_proofs(proofs)
|
||||
else:
|
||||
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Cashu: {str(e)}",
|
||||
)
|
||||
finally:
|
||||
logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}")
|
||||
# delete proofs from pending list
|
||||
await ledger._unset_proofs_pending(proofs)
|
||||
|
||||
@@ -375,7 +418,7 @@ async def split(
|
||||
)
|
||||
|
||||
amount = payload.amount
|
||||
outputs = payload.outputs.blinded_messages
|
||||
outputs = payload.outputs
|
||||
assert outputs, Exception("no outputs provided.")
|
||||
split_return = None
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Streamer Copilot",
|
||||
"short_description": "Video tips/animations/webhooks",
|
||||
"icon": "face",
|
||||
"tile": "/copilot/static/bitcoin-streaming.png",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse # type: ignore
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
|
||||
@@ -75,7 +75,7 @@ async def lnurl_callback(
|
||||
memo=cp.lnurl_title,
|
||||
unhashed_description=(
|
||||
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
||||
).encode("utf-8"),
|
||||
).encode(),
|
||||
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
|
||||
)
|
||||
payResponse = {"pr": payment_request, "routes": []}
|
||||
|
||||
@@ -4,11 +4,11 @@ from typing import Dict, Optional
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
|
||||
|
||||
class CreateCopilotData(BaseModel):
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -24,12 +24,12 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
webhook = None
|
||||
data = None
|
||||
if not payment.extra or payment.extra.get("tag") != "copilot":
|
||||
if payment.extra.get("tag") != "copilot":
|
||||
# not an copilot invoice
|
||||
return
|
||||
|
||||
webhook = None
|
||||
data = None
|
||||
copilot = await get_copilot(payment.extra.get("copilotid", -1))
|
||||
|
||||
if not copilot:
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||
animation<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
Created by,
|
||||
<a class="text-secondary" href="https://github.com/benarc"
|
||||
>Ben Arc</a
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
<div
|
||||
v-if="copilot.lnurl_toggle == 1"
|
||||
class="rounded-borders column fixed-right"
|
||||
id="draggableqr"
|
||||
class="rounded-borders"
|
||||
style="
|
||||
width: 250px;
|
||||
background-color: white;
|
||||
@@ -25,7 +26,7 @@
|
||||
>
|
||||
<div class="col">
|
||||
<qrcode
|
||||
:value="copilot.lnurl"
|
||||
:value="'lightning:' + copilot.lnurl"
|
||||
:options="{width:250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
@@ -36,8 +37,9 @@
|
||||
</div>
|
||||
|
||||
<h2
|
||||
id="draggableprice"
|
||||
v-if="copilot.show_price != 0"
|
||||
class="text-bold fixed-bottom-left"
|
||||
class="text-bold"
|
||||
style="
|
||||
margin: 60px 60px;
|
||||
font-size: 110px;
|
||||
@@ -47,6 +49,7 @@
|
||||
>
|
||||
{% raw %}{{ price }}{% endraw %}
|
||||
</h2>
|
||||
|
||||
<p
|
||||
v-if="copilot.show_ack != 0"
|
||||
class="fixed-top"
|
||||
@@ -73,7 +76,19 @@
|
||||
.q-page {
|
||||
padding: 0px;
|
||||
}
|
||||
#draggableqr {
|
||||
width: 250px;
|
||||
height: 300px;
|
||||
cursor: grab;
|
||||
}
|
||||
#draggableprice {
|
||||
width: 550px;
|
||||
height: 60px;
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
@@ -93,9 +108,8 @@
|
||||
},
|
||||
methods: {
|
||||
showNotif: function (userMessage) {
|
||||
var colour = this.colours[
|
||||
Math.floor(Math.random() * this.colours.length)
|
||||
]
|
||||
var colour =
|
||||
this.colours[Math.floor(Math.random() * this.colours.length)]
|
||||
this.$q.notify({
|
||||
color: colour,
|
||||
icon: 'chat_bubble_outline',
|
||||
@@ -167,7 +181,11 @@
|
||||
this.initCamera()
|
||||
},
|
||||
created: function () {
|
||||
self = this
|
||||
$(function () {
|
||||
$('#draggableqr').draggable()
|
||||
$('#draggableprice').draggable()
|
||||
}),
|
||||
(self = this)
|
||||
self.copilot = JSON.parse(localStorage.getItem('copilot'))
|
||||
LNbits.api
|
||||
.request(
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse # type: ignore
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import copilot_ext, copilot_renderer
|
||||
from .crud import get_copilot
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return copilot_renderer().TemplateResponse(
|
||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Query, Request
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import websocketUpdater
|
||||
@@ -22,9 +20,7 @@ from .models import CreateCopilotData
|
||||
|
||||
|
||||
@copilot_ext.get("/api/v1/copilot")
|
||||
async def api_copilots_retrieve(
|
||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
async def api_copilots_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
wallet_user = wallet.wallet.user
|
||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||
try:
|
||||
@@ -37,7 +33,7 @@ async def api_copilots_retrieve(
|
||||
async def api_copilot_retrieve(
|
||||
req: Request,
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
@@ -54,7 +50,7 @@ async def api_copilot_retrieve(
|
||||
async def api_copilot_create_or_update(
|
||||
data: CreateCopilotData,
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
data.user = wallet.wallet.user
|
||||
data.wallet = wallet.wallet.id
|
||||
@@ -68,7 +64,7 @@ async def api_copilot_create_or_update(
|
||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||
async def api_copilot_delete(
|
||||
copilot_id: str = Query(None),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Deezy: Home for Lightning Liquidity
|
||||
Swap lightning bitcoin for on-chain bitcoin to get inbound liquidity. Or get an on-chain deposit address for your lightning address.
|
||||
* [Website](https://deezy.io)
|
||||
* [Lightning Node](https://amboss.space/node/024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf)
|
||||
* [Documentation](https://docs.deezy.io)
|
||||
* [Discord](https://discord.gg/nEBbrUAvPy)
|
||||
|
||||
# Usage
|
||||
This extension lets you swap lightning btc for on-chain btc and vice versa.
|
||||
* Swap Lightning -> BTC to get inbound liquidity
|
||||
* Swap BTC -> Lightning to generate an on-chain deposit address for your lightning address
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_deezy")
|
||||
|
||||
deezy_ext: APIRouter = APIRouter(prefix="/deezy", tags=["deezy"])
|
||||
|
||||
deezy_static_files = [
|
||||
{
|
||||
"path": "/deezy/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/deezy/static"),
|
||||
"name": "deezy_static",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def deezy_renderer():
|
||||
return template_renderer(["lnbits/extensions/deezy/templates"])
|
||||
|
||||
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Deezy",
|
||||
"short_description": "LN to onchain, onchain to LN swaps",
|
||||
"tile": "/deezy/static/deezy.png",
|
||||
"contributors": ["Uthpala"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user