Compare commits
615 Commits
jit-backup
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395495fef5 | ||
|
|
aee8c5f032 | ||
|
|
0785a1ab39 | ||
|
|
ae06fd4404 | ||
|
|
e2d9b4e87c | ||
|
|
621adc1804 | ||
|
|
831f1e2c5b | ||
|
|
9f56451b41 | ||
|
|
427ba10f5e | ||
|
|
409822d1a2 | ||
|
|
1e84841af1 | ||
|
|
29136fe4c4 | ||
|
|
1b462e81da | ||
|
|
e75d011eb4 | ||
|
|
ef534215f9 | ||
|
|
b147ddba3f | ||
|
|
fdd3e0fc33 | ||
|
|
797e7a33c7 | ||
|
|
f4f988b113 | ||
|
|
b9000dfc75 | ||
|
|
9d44aee152 | ||
|
|
698afcd033 | ||
|
|
ac98a426c0 | ||
|
|
d232fbc68c | ||
|
|
c9d6f5d9d7 | ||
|
|
336daad4c6 | ||
|
|
4dbab4038c | ||
|
|
5b86d4f28f | ||
|
|
f64768a383 | ||
|
|
ccd160c4e0 | ||
|
|
5bd52b6f86 | ||
|
|
c2e7a79bb8 | ||
|
|
4a0bedd7b0 | ||
|
|
7f917a2a92 | ||
|
|
957da11d89 | ||
|
|
b489cd0ec7 | ||
|
|
f86b996303 | ||
|
|
5f59fa7987 | ||
|
|
48368cecbb | ||
|
|
df637acb54 | ||
|
|
9b8890d04e | ||
|
|
b5ce32c67b | ||
|
|
ad870cae50 | ||
|
|
612ef699b7 | ||
|
|
186e995c41 | ||
|
|
489d78bc40 | ||
|
|
74ec312c31 | ||
|
|
796a512b50 | ||
|
|
fd3bbf68e5 | ||
|
|
2061e93db3 | ||
|
|
3834ef4c4f | ||
|
|
cdf6c78b0a | ||
|
|
b0d1e83bda | ||
|
|
a678ad4d9e | ||
|
|
855046c161 | ||
|
|
f55c394c0c | ||
|
|
f5486a087b | ||
|
|
0b04dd03d4 | ||
|
|
85ca2d9658 | ||
|
|
72885feea7 | ||
|
|
0aa42e93a8 | ||
|
|
ef7faed0d8 | ||
|
|
d63ef814ca | ||
|
|
4868cf9b1f | ||
|
|
d076afbd85 | ||
|
|
3e6db4d9e9 | ||
|
|
22f1da33f3 | ||
|
|
d9dc06eb01 | ||
|
|
f94a1aa262 | ||
|
|
4e047acc8e | ||
|
|
e4e68c1cf4 | ||
|
|
28af1bfecb | ||
|
|
4e54057447 | ||
|
|
25eadbd822 | ||
|
|
e87131338b | ||
|
|
9ff0c0166b | ||
|
|
ea8344f537 | ||
|
|
6698d81efa | ||
|
|
426f50e1cd | ||
|
|
42af818d85 | ||
|
|
8babd11c1e | ||
|
|
244682f390 | ||
|
|
dfeb073c3c | ||
|
|
8dd2a33d16 | ||
|
|
3b3d82f873 | ||
|
|
d24e971a09 | ||
|
|
206b569fd4 | ||
|
|
cad7b343e4 | ||
|
|
18acd479f1 | ||
|
|
553948a38c | ||
|
|
375a320fb0 | ||
|
|
e00b9b8d7b | ||
|
|
fe33ec8769 | ||
|
|
df790001c0 | ||
|
|
83b567d02c | ||
|
|
712f2a5e5c | ||
|
|
128ee5b744 | ||
|
|
ab0bb4156a | ||
|
|
c5446985f9 | ||
|
|
ff1489fba7 | ||
|
|
7cf70000f3 | ||
|
|
e0b07afc80 | ||
|
|
7f4e12dd2c | ||
|
|
c7f2c4781a | ||
|
|
903869d04a | ||
|
|
3a8727ade2 | ||
|
|
71a9bf4cc5 | ||
|
|
1375719773 | ||
|
|
90bd8f0f6f | ||
|
|
6bcc0547d6 | ||
|
|
80076556e1 | ||
|
|
13ffd0371b | ||
|
|
75c87e96ed | ||
|
|
cb6f10999a | ||
|
|
63235396af | ||
|
|
7247bbc409 | ||
|
|
49d5ef1e47 | ||
|
|
ef9a3cd062 | ||
|
|
c4aa16992c | ||
|
|
46c4eead09 | ||
|
|
3de73adb19 | ||
|
|
ccd80fad4c | ||
|
|
34f28284a0 | ||
|
|
a3e9a36dbe | ||
|
|
9faf94b12c | ||
|
|
747e9bd1db | ||
|
|
5c8453c3e3 | ||
|
|
96cbcb5f88 | ||
|
|
afe395c9ff | ||
|
|
4efb05744b | ||
|
|
386f9fa774 | ||
|
|
a4a5d9fbdc | ||
|
|
e3412b1433 | ||
|
|
ea55664f93 | ||
|
|
866532332e | ||
|
|
f93b676f40 | ||
|
|
c3fc4fc82b | ||
|
|
2da390c091 | ||
|
|
b76e9392a6 | ||
|
|
3c2295f744 | ||
|
|
ca50033d21 | ||
|
|
5faf6b21c5 | ||
|
|
4aef5c078c | ||
|
|
c6c1b49cd2 | ||
|
|
4c3860edf6 | ||
|
|
7adeb99e66 | ||
|
|
401fc3f1a4 | ||
|
|
c38ad051e4 | ||
|
|
6d7d708e71 | ||
|
|
f573916392 | ||
|
|
ffeb9fbea8 | ||
|
|
01586fd857 | ||
|
|
81daa88618 | ||
|
|
ef093a480b | ||
|
|
ee8b8b991b | ||
|
|
11f621ad9e | ||
|
|
0bedbc5cb2 | ||
|
|
be8f7ad5cc | ||
|
|
7db3b597ea | ||
|
|
b03987cb87 | ||
|
|
f0584a032e | ||
|
|
44bda68311 | ||
|
|
5d20274f4c | ||
|
|
1d4797d1da | ||
|
|
abc7713131 | ||
|
|
2fdf6394a3 | ||
|
|
4caf6baedd | ||
|
|
e29d57ab80 | ||
|
|
85d89ceeb6 | ||
|
|
1eb87753f3 | ||
|
|
e5ccf2f579 | ||
|
|
f92294e4e7 | ||
|
|
42d3e36eb9 | ||
|
|
52c6f0ed6d | ||
|
|
b5810cb433 | ||
|
|
9c4836fbb6 | ||
|
|
a32866ba9c | ||
|
|
4e53925020 | ||
|
|
e36d83d54e | ||
|
|
a5421a92e6 | ||
|
|
a13156c1d3 | ||
|
|
b5c6ae88f3 | ||
|
|
921340f15a | ||
|
|
e1c60245a8 | ||
|
|
7623a9e6ce | ||
|
|
7e4871bda0 | ||
|
|
0e974b4848 | ||
|
|
99d5b33756 | ||
|
|
4ad3a9b333 | ||
|
|
1625f8817e | ||
|
|
1e081c4f0a | ||
|
|
ab79da7ba4 | ||
|
|
730e355b0f | ||
|
|
8cc75c2941 | ||
|
|
305acc2488 | ||
|
|
f869b6cb26 | ||
|
|
ebb64e05c6 | ||
|
|
336bafd0f5 | ||
|
|
5314b03cf6 | ||
|
|
19812fc880 | ||
|
|
13c7a2aed4 | ||
|
|
de70c59a26 | ||
|
|
f85f361300 | ||
|
|
8bc4c07edf | ||
|
|
8012a810f1 | ||
|
|
3c4ce6c026 | ||
|
|
7ff8b56cee | ||
|
|
11981fde2e | ||
|
|
3f96a1717e | ||
|
|
436d398bd4 | ||
|
|
cd8c95ba8c | ||
|
|
f73d964d6f | ||
|
|
9bf84303fb | ||
|
|
151f7059d8 | ||
|
|
9de6dd42fa | ||
|
|
0f0fe8d064 | ||
|
|
c4d2ce57c6 | ||
|
|
69fedc0767 | ||
|
|
a9da978061 | ||
|
|
d29bef6ce6 | ||
|
|
9bb42417b8 | ||
|
|
39895f780b | ||
|
|
0b7c25aa54 | ||
|
|
96dd48f6ba | ||
|
|
66befda61f | ||
|
|
3c413fb2c9 | ||
|
|
c54c3e3aea | ||
|
|
82746713d0 | ||
|
|
f1a149ffa3 | ||
|
|
f9e7e82e50 | ||
|
|
dd1ce2cc94 | ||
|
|
113eecae61 | ||
|
|
c1dce9f922 | ||
|
|
02e5f960a6 | ||
|
|
9e6aa4d7e0 | ||
|
|
00d221d411 | ||
|
|
604455cdd5 | ||
|
|
647662f12a | ||
|
|
2aa4c2473b | ||
|
|
c75eca0c21 | ||
|
|
8dca3455a8 | ||
|
|
582d14aaca | ||
|
|
b096d83a5e | ||
|
|
d53616224d | ||
|
|
72fcfad8d6 | ||
|
|
19d7bb8736 | ||
|
|
2304f76f6c | ||
|
|
71404327c1 | ||
|
|
68137f96f1 | ||
|
|
c1400db8a9 | ||
|
|
e75fd6b78f | ||
|
|
d30d6bd4f9 | ||
|
|
29bc2a4924 | ||
|
|
cbceadbe6c | ||
|
|
515a765232 | ||
|
|
b46bbaec50 | ||
|
|
cceb71956a | ||
|
|
a0ddb8d2b2 | ||
|
|
5ff026d263 | ||
|
|
697466f317 | ||
|
|
fe084a41fd | ||
|
|
b8ddba72b1 | ||
|
|
c38e944bcf | ||
|
|
57355d4e6b | ||
|
|
7554573605 | ||
|
|
05de7d871b | ||
|
|
4287d3b9a3 | ||
|
|
87f9de6d49 | ||
|
|
495fa9175f | ||
|
|
3b493ab98e | ||
|
|
172b3e72a1 | ||
|
|
b914cc0d8d | ||
|
|
8de8c62027 | ||
|
|
9603cf9d11 | ||
|
|
94af2bf529 | ||
|
|
fdd84c9f17 | ||
|
|
933e74649b | ||
|
|
83e4434f7b | ||
|
|
a3e49c9c2b | ||
|
|
8d0880ac85 | ||
|
|
cac99f6e87 | ||
|
|
550db94272 | ||
|
|
277386d333 | ||
|
|
6b03fdb4bc | ||
|
|
2e9f628f9c | ||
|
|
58334b1b37 | ||
|
|
ef76b76876 | ||
|
|
f796edf061 | ||
|
|
5fc526a999 | ||
|
|
42e6cbebef | ||
|
|
ce7e7f46b3 | ||
|
|
3780883fa7 | ||
|
|
b30f8b1bb8 | ||
|
|
454e556e56 | ||
|
|
4d7d14571f | ||
|
|
28fb83c8da | ||
|
|
f46e0752d1 | ||
|
|
e10d8b6157 | ||
|
|
d5605021d4 | ||
|
|
e1b34432a0 | ||
|
|
44170d0e0f | ||
|
|
8162f0a337 | ||
|
|
188b995caf | ||
|
|
5fb4b6efef | ||
|
|
49128f09e1 | ||
|
|
8d46bde0a7 | ||
|
|
61254044a3 | ||
|
|
d73a3e0fd0 | ||
|
|
a9a8e6d83f | ||
|
|
53eb9c2e29 | ||
|
|
976ad3c8b3 | ||
|
|
c635520200 | ||
|
|
3142f3396b | ||
|
|
639e3ad66c | ||
|
|
6316a1e92c | ||
|
|
b6e7777c40 | ||
|
|
1b3ca22d6e | ||
|
|
c6b0feb9a2 | ||
|
|
c411e75a3e | ||
|
|
ff824a7d3b | ||
|
|
c942fe35d8 | ||
|
|
0a8549bc19 | ||
|
|
42d0b66ce9 | ||
|
|
057ef9889d | ||
|
|
07c9da242d | ||
|
|
9b1e328ca9 | ||
|
|
b9be473bd4 | ||
|
|
bff9ae800f | ||
|
|
8fb53df259 | ||
|
|
d0aac01f74 | ||
|
|
b7d1d61900 | ||
|
|
e02f31e5fe | ||
|
|
fa1f5b3997 | ||
|
|
2d871ce982 | ||
|
|
4bd66e783a | ||
|
|
34f0cb3a6e | ||
|
|
74d9554dc1 | ||
|
|
6b8547fe04 | ||
|
|
2a6126bdef | ||
|
|
386dd80925 | ||
|
|
d1405d6d88 | ||
|
|
6753e5507f | ||
|
|
a57b451386 | ||
|
|
3f4f2e0a57 | ||
|
|
fecd4c59c6 | ||
|
|
c7f1c060d6 | ||
|
|
03b4640c22 | ||
|
|
6b189bcec3 | ||
|
|
75d7a9bd97 | ||
|
|
9dff2cf230 | ||
|
|
15d4d1d228 | ||
|
|
d835c7cc75 | ||
|
|
1f23480b60 | ||
|
|
091e383a07 | ||
|
|
d9ed601377 | ||
|
|
1182ed7e40 | ||
|
|
c7b567c9f0 | ||
|
|
ba06a6f5db | ||
|
|
5c6265e012 | ||
|
|
4257d85bd9 | ||
|
|
d2c445a864 | ||
|
|
f90f64185e | ||
|
|
02fd84efca | ||
|
|
409eddf608 | ||
|
|
92274d1989 | ||
|
|
48108e7a35 | ||
|
|
27dc922beb | ||
|
|
b8072c81df | ||
|
|
fd82c59dd7 | ||
|
|
567f2e967a | ||
|
|
5a84e7802c | ||
|
|
0c651cb687 | ||
|
|
951636a25e | ||
|
|
7d96c69d4f | ||
|
|
9f244cbb2f | ||
|
|
12120f625b | ||
|
|
61e21abcec | ||
|
|
357d700d19 | ||
|
|
2acc63f3f1 | ||
|
|
2818207355 | ||
|
|
64b46e45f0 | ||
|
|
b05b54d1ee | ||
|
|
f87076eec2 | ||
|
|
03a60b22f9 | ||
|
|
6e0625c9e5 | ||
|
|
0e5a23d160 | ||
|
|
e862992c09 | ||
|
|
1143f28eba | ||
|
|
ac56558028 | ||
|
|
7cf2f85ad5 | ||
|
|
66ca7ed6d3 | ||
|
|
4ff0901e7e | ||
|
|
ad49ba855a | ||
|
|
bde26040ce | ||
|
|
3d3933b464 | ||
|
|
5587440432 | ||
|
|
3eef655ac5 | ||
|
|
595ca7c646 | ||
|
|
62b5b850df | ||
|
|
0f848cf6b1 | ||
|
|
c3c91a9c58 | ||
|
|
5bbbed8963 | ||
|
|
5c480024d2 | ||
|
|
8269a9d3ea | ||
|
|
610acc0542 | ||
|
|
b6a96dac7b | ||
|
|
ff0708a32c | ||
|
|
6f4dbcb397 | ||
|
|
6b6de10474 | ||
|
|
772471f435 | ||
|
|
31d855465f | ||
|
|
6318b706e6 | ||
|
|
1c7e57b093 | ||
|
|
f144c2137b | ||
|
|
837f4163cd | ||
|
|
d4b2954d34 | ||
|
|
416ffddebc | ||
|
|
edccc495aa | ||
|
|
b2d5777922 | ||
|
|
80afbb7a22 | ||
|
|
96d7f18856 | ||
|
|
2d8a7c5498 | ||
|
|
fc8c42e17b | ||
|
|
1298fb7d68 | ||
|
|
c2ca7e2d09 | ||
|
|
06c5989acd | ||
|
|
70e4eb29ed | ||
|
|
cf5ab8f5a3 | ||
|
|
b7368b4cfa | ||
|
|
1c4e14e265 | ||
|
|
715037affa | ||
|
|
d717333bac | ||
|
|
a413bf7b62 | ||
|
|
ae8a871826 | ||
|
|
f4e4c317d4 | ||
|
|
f177dfd037 | ||
|
|
15429277a7 | ||
|
|
1a9a8c8126 | ||
|
|
6dab1df016 | ||
|
|
6f65c41079 | ||
|
|
8d08227b9b | ||
|
|
a85373ee35 | ||
|
|
49ef23f0e1 | ||
|
|
95dc670214 | ||
|
|
69560adb49 | ||
|
|
710a5a7d77 | ||
|
|
c9d06fa099 | ||
|
|
3b10624617 | ||
|
|
4f00e50c94 | ||
|
|
12b467e02f | ||
|
|
0821c79b62 | ||
|
|
21eea05f4d | ||
|
|
133125b6da | ||
|
|
6e3ff406b5 | ||
|
|
66218bbcd7 | ||
|
|
6812c535de | ||
|
|
00f88d4471 | ||
|
|
998eccd038 | ||
|
|
5a450d18a4 | ||
|
|
b71773f6ac | ||
|
|
9d0ded4399 | ||
|
|
c851b33984 | ||
|
|
1e8deb38d0 | ||
|
|
a072511751 | ||
|
|
5b7b0e5e94 | ||
|
|
3b9ab06704 | ||
|
|
da8d3bb513 | ||
|
|
074876e544 | ||
|
|
b649596dc2 | ||
|
|
ed124079a6 | ||
|
|
b0b70a3d95 | ||
|
|
c117e27872 | ||
|
|
dbfbed666d | ||
|
|
20fd84542d | ||
|
|
f7bef6f582 | ||
|
|
3260ad02ef | ||
|
|
61d4cb9a76 | ||
|
|
67e90e6b98 | ||
|
|
fcec8cb549 | ||
|
|
76d5dd225b | ||
|
|
77766a7923 | ||
|
|
e620ab832a | ||
|
|
330c467adf | ||
|
|
65c1f52e25 | ||
|
|
716531984e | ||
|
|
ff3db4d7f7 | ||
|
|
1461108a2e | ||
|
|
efca7ca412 | ||
|
|
6a9269d908 | ||
|
|
dde1ae2c59 | ||
|
|
156c49aa75 | ||
|
|
22766308e9 | ||
|
|
ac421fc050 | ||
|
|
ae7bd120d4 | ||
|
|
1c737363e1 | ||
|
|
1432e8a526 | ||
|
|
095e618239 | ||
|
|
00d9855883 | ||
|
|
34002b4d84 | ||
|
|
cd5d78b27c | ||
|
|
acecbfb4ab | ||
|
|
35e4f91420 | ||
|
|
be7c8047a9 | ||
|
|
51d2f71f9d | ||
|
|
b6c668bcd8 | ||
|
|
28fd2a44b6 | ||
|
|
65ca9237f6 | ||
|
|
45cb172521 | ||
|
|
e4c9a6f3c9 | ||
|
|
28d05bd4f1 | ||
|
|
d07f01d116 | ||
|
|
0c2694548c | ||
|
|
aca73d1724 | ||
|
|
df468b0476 | ||
|
|
1aa98fc243 | ||
|
|
364e26112e | ||
|
|
63bf13ae8f | ||
|
|
d1f2b7a7dd | ||
|
|
dbb105491a | ||
|
|
b6b4dad7a7 | ||
|
|
747ef70f87 | ||
|
|
bf98bb8f13 | ||
|
|
5c10767939 | ||
|
|
f917f42bdd | ||
|
|
707e5c398f | ||
|
|
1cb3e82d51 | ||
|
|
d8a2144bbf | ||
|
|
80e4c2b805 | ||
|
|
6921484099 | ||
|
|
9c721eb273 | ||
|
|
e22a25bc27 | ||
|
|
e5cea23d0b | ||
|
|
09dfb8a28f | ||
|
|
9d6d975c8e | ||
|
|
84d913a180 | ||
|
|
750e1676a3 | ||
|
|
35576385e9 | ||
|
|
1f9d599229 | ||
|
|
4aeb4136ab | ||
|
|
61f5383fc2 | ||
|
|
1c6875d8c6 | ||
|
|
d51a28c635 | ||
|
|
9d6f3cc8e6 | ||
|
|
1efd8a5a21 | ||
|
|
e9d912cbe7 | ||
|
|
4f47d3ee55 | ||
|
|
0394d84eef | ||
|
|
772015d56f | ||
|
|
eb4bea4381 | ||
|
|
f204dc76cd | ||
|
|
2f86950d6d | ||
|
|
982e625ca3 | ||
|
|
34a7777297 | ||
|
|
d33b3734ab | ||
|
|
199f480936 | ||
|
|
56a75a07b8 | ||
|
|
8b63001c61 | ||
|
|
bf687d516f | ||
|
|
c7deb7e381 | ||
|
|
de5c4d1a45 | ||
|
|
686796f347 | ||
|
|
a192ca2b49 | ||
|
|
6d7eed333d | ||
|
|
4af0a90e07 | ||
|
|
f13649238d | ||
|
|
28b5272ce5 | ||
|
|
77e9a81b3d | ||
|
|
ac2f058bfe | ||
|
|
e6e6ccb31c | ||
|
|
c0b215efcd | ||
|
|
3f1c1968b9 | ||
|
|
4fa6a708cb | ||
|
|
5178b0f190 | ||
|
|
b8834f8411 | ||
|
|
630679028e | ||
|
|
ec04028b8e | ||
|
|
628c16015e | ||
|
|
988d00e29a | ||
|
|
315e517987 | ||
|
|
ed6e51a744 | ||
|
|
12518ee193 | ||
|
|
e043d57e1c | ||
|
|
24a160a1c0 | ||
|
|
21a1d8ad7b | ||
|
|
e8c816c04a | ||
|
|
a6acf2d8d0 | ||
|
|
94e8bc624b | ||
|
|
72b1c02780 | ||
|
|
586beb5ec8 | ||
|
|
a8d29eb123 | ||
|
|
9ddf88db27 | ||
|
|
6dd14b482a | ||
|
|
ed553ca574 | ||
|
|
564649f4af | ||
|
|
313e3cf666 | ||
|
|
935ec8c1c0 | ||
|
|
8d4f6547b2 | ||
|
|
3ced11cc0c | ||
|
|
a239f92aed | ||
|
|
f0f4feef41 | ||
|
|
933280396c | ||
|
|
864013da9b | ||
|
|
d4954659b7 | ||
|
|
d85ffe0704 | ||
|
|
d55a87f167 | ||
|
|
e3438ff78c | ||
|
|
8af8cf5757 | ||
|
|
c731a9574d | ||
|
|
8e3fb455a6 | ||
|
|
35165b7fc8 | ||
|
|
be6559fc08 | ||
|
|
1ae53b491e | ||
|
|
79489ae26c | ||
|
|
eba2684b6a | ||
|
|
05164ec471 |
310
.github/workflows/build-test.yml
vendored
Normal file
310
.github/workflows/build-test.yml
vendored
Normal file
@ -0,0 +1,310 @@
|
||||
name: 'Test & Build the app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
test-desktop:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: true
|
||||
BTCPAY_SERVER_URL: http://localhost:14142
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# Setup infrastructure
|
||||
- name: Start containers
|
||||
run: |
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" build
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
./setup.sh
|
||||
cd submodules/btcpayserver
|
||||
# Start non-HTTPS to avoid certificate errors
|
||||
nohup dotnet run -c Debug --project BTCPayServer --launch-profile Bitcoin &
|
||||
while ! curl -s -k http://localhost:14142/api/v1/health > /dev/null; do
|
||||
echo "Waiting for BTCPay Server to start..."
|
||||
sleep 10
|
||||
done
|
||||
# Run tests
|
||||
- name: Run tests
|
||||
run: |
|
||||
dotnet test -c Release -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
|
||||
build-android:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui
|
||||
- name: Clean before build
|
||||
run: |
|
||||
dotnet clean BTCPayApp.Maui/BTCPayApp.Maui.csproj
|
||||
- name: Build
|
||||
# TODO: Use proper keystore once we switch to real releases
|
||||
# https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-8.0#code-try-4
|
||||
run: |
|
||||
dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-android -c Release -o publish/android
|
||||
env:
|
||||
ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: org.btcpayserver.BTCPayApp-Signed.apk
|
||||
path: publish/android/org.btcpayserver.BTCPayApp-Signed.apk
|
||||
- name: Create pre-release
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
automatic_release_tag: "latest"
|
||||
prerelease: true
|
||||
title: "Development Build"
|
||||
files: |
|
||||
publish/android/org.btcpayserver.BTCPayApp-Signed.apk
|
||||
|
||||
# build-desktop-win:
|
||||
# runs-on: windows-latest
|
||||
# steps:
|
||||
# # Setup code, .NET and Android
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Build win x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x64 -c Debug -o publish/win-x64
|
||||
# - name: Build win x86
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-x86 -c Debug -o publish/win-x86
|
||||
# - name: Build win-arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r win-arm64 -c Debug -o publish/win-arm64
|
||||
# - name: Upload artifact win
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: windows build
|
||||
# path: |
|
||||
# publish/win-x64
|
||||
# publish/win-x86
|
||||
# publish/win-arm64
|
||||
|
||||
# build-desktop-linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# # Setup code, .NET and Android
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Build linux x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-x64 -c Debug -o publish/linux-x64
|
||||
# - name: Build linux arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -r linux-arm64 -c Debug -o publish/linux-arm64
|
||||
# - name: Upload artifact linux
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: linux build
|
||||
# path: |
|
||||
# publish/linux-x64
|
||||
# publish/linux-arm64
|
||||
|
||||
# build-desktop-mac:
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# # Checkout the code
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# # Import code-signing certificates
|
||||
# - name: Import Code-Signing Certificates
|
||||
# uses: apple-actions/import-codesign-certs@v3
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
|
||||
# # Verify imported certificates and extract Developer ID
|
||||
# - name: Find Code Signing Certificate
|
||||
# id: find-cert
|
||||
# run: |
|
||||
# CERT_NAME=$(security find-identity -v -p codesigning | grep "Apple Distribution" | awk -F '"Apple Distribution: ' '{print $2}' | awk -F '"' '{print $1}')
|
||||
# if [ -z "$CERT_NAME" ]; then
|
||||
# echo "No valid Apple Distribution certificate found!"
|
||||
# exit 1
|
||||
# fi
|
||||
# echo "Certificate Name: Apple Distribution: $CERT_NAME"
|
||||
# echo "CERT_NAME=Apple Distribution: $CERT_NAME" >> $GITHUB_ENV
|
||||
# # Setup .NET
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# # Build the app for macOS architectures
|
||||
# - name: Build mac x64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-x64 -o publish/osx-x64
|
||||
# - name: Build mac arm64
|
||||
# run: dotnet publish BTCPayApp.Photino/BTCPayApp.Photino.csproj -c Release --self-contained -r osx-arm64 -o publish/osx-arm64
|
||||
# # Create the .app bundle
|
||||
# - name: Create .app bundle for x64
|
||||
# run: |
|
||||
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/MacOS
|
||||
# mkdir -p dist/osx-x64/BTCPayApp.app/Contents/Resources
|
||||
# ls -lA publish/osx-x64/
|
||||
# cp -R publish/osx-x64/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# #cp -R publish/osx-x64/* dist/osx-x64/BTCPayApp.app/Contents/MacOS/
|
||||
# #mv dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-x64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
# <plist version=\"1.0\">
|
||||
# <dict>
|
||||
# <key>CFBundleExecutable</key>
|
||||
# <string>BTCPayApp</string>
|
||||
# <key>CFBundleIdentifier</key>
|
||||
# <string>org.btcpayserver.app</string>
|
||||
# <key>CFBundleName</key>
|
||||
# <string>BTCPay App</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>1.0</string>
|
||||
# <key>CFBundlePackageType</key>
|
||||
# <string>APPL</string>
|
||||
# </dict>
|
||||
# </plist>" > dist/osx-x64/BTCPayApp.app/Contents/Info.plist
|
||||
# cat dist/osx-x64/BTCPayApp.app/Contents/Info.plist
|
||||
# ls -lA dist/osx-x64/BTCPayApp.app/Contents/**
|
||||
# - name: Create .app bundle for arm64
|
||||
# run: |
|
||||
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/MacOS
|
||||
# mkdir -p dist/osx-arm64/BTCPayApp.app/Contents/Resources
|
||||
# ls -lA publish/osx-arm64/
|
||||
# cp -R publish/osx-arm64/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# #cp -R publish/osx-arm64/* dist/osx-arm64/BTCPayApp.app/Contents/MacOS/
|
||||
# #mv dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp.Photino dist/osx-arm64/BTCPayApp.app/Contents/MacOS/BTCPayApp
|
||||
# echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
# <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
# <plist version=\"1.0\">
|
||||
# <dict>
|
||||
# <key>CFBundleExecutable</key>
|
||||
# <string>BTCPayApp</string>
|
||||
# <key>CFBundleIdentifier</key>
|
||||
# <string>org.btcpayserver.app</string>
|
||||
# <key>CFBundleName</key>
|
||||
# <string>BTCPay App</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>1.0</string>
|
||||
# <key>CFBundlePackageType</key>
|
||||
# <string>APPL</string>
|
||||
# </dict>
|
||||
# </plist>" > dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
|
||||
# cat dist/osx-arm64/BTCPayApp.app/Contents/Info.plist
|
||||
# ls -lA dist/osx-arm64/BTCPayApp.app/Contents/**
|
||||
# # Sign the .app bundles using the dynamic certificate name
|
||||
# - name: Sign x64 app bundle
|
||||
# run: |
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-x64/BTCPayApp.app
|
||||
# codesign --verify --deep --strict dist/osx-x64/BTCPayApp.app
|
||||
# - name: Sign arm64 app bundle
|
||||
# run: |
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dist/osx-arm64/BTCPayApp.app
|
||||
# codesign --verify --deep --strict dist/osx-arm64/BTCPayApp.app
|
||||
# # Verify app bundle signing
|
||||
# - name: Verify x64 app bundle signing
|
||||
# run: spctl --assess --type execute dist/osx-x64/BTCPayApp.app
|
||||
# continue-on-error: true
|
||||
# - name: Verify arm64 app bundle signing
|
||||
# run: spctl --assess --type execute dist/osx-arm64/BTCPayApp.app
|
||||
# continue-on-error: true
|
||||
# # Create DMG files
|
||||
# - name: Create DMG for x64
|
||||
# run: |
|
||||
# mkdir -p dmg
|
||||
# hdiutil create -size 1gb -volname "BTCPayApp-osx-x64" -srcfolder "dist/osx-x64" -ov -format UDZO dmg/BTCPayApp-x64.dmg
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-x64.dmg
|
||||
# codesign --verify --deep --strict dmg/BTCPayApp-x64.dmg
|
||||
# - name: Create DMG for arm64
|
||||
# run: |
|
||||
# mkdir -p dmg
|
||||
# hdiutil create -size 1gb -volname "BTCPayApp-osx-arm64" -srcfolder "dist/osx-arm64" -ov -format UDZO dmg/BTCPayApp-arm64.dmg
|
||||
# codesign --sign "$CERT_NAME" --deep --force --options runtime dmg/BTCPayApp-arm64.dmg
|
||||
# codesign --verify --deep --strict dmg/BTCPayApp-arm64.dmg
|
||||
# # Verify DMG signing
|
||||
# - name: Verify x64 DMG signing
|
||||
# run: spctl --assess --type execute dmg/BTCPayApp-x64.dmg
|
||||
# continue-on-error: true
|
||||
# - name: Verify arm64 DMG signing
|
||||
# run: spctl --assess --type execute dmg/BTCPayApp-arm64.dmg
|
||||
# continue-on-error: true
|
||||
# # Upload artifacts
|
||||
# - name: Upload DMG artifacts
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: mac-dmg
|
||||
# path: dmg
|
||||
#
|
||||
# build-ios:
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# submodules: recursive
|
||||
# - name: Setup Xcode version
|
||||
# uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
# with:
|
||||
# xcode-version: 16.0
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
# - name: Install workloads
|
||||
# run: dotnet workload install maui
|
||||
# - name: Import Code-Signing Certificates
|
||||
# uses: Apple-Actions/import-codesign-certs@v1
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.APPLE_CERT_P12_PASS }}
|
||||
# - name: Download Apple Provisioning Profiles
|
||||
# uses: Apple-Actions/download-provisioning-profiles@v1
|
||||
# with:
|
||||
# bundle-id: ${{ secrets.APPLE_BUNDLE_ID }}
|
||||
# issuer-id: ${{ secrets.APPLE_ISSUER_ID }}
|
||||
# api-key-id: ${{ secrets.APPLE_KEY_ID }}
|
||||
# api-private-key: ${{ secrets.APPLE_KEY }}
|
||||
# - name: Build
|
||||
# run: dotnet publish BTCPayApp.Maui/BTCPayApp.Maui.csproj -f net8.0-ios -c Debug -o publish/ios
|
||||
# - name: Upload artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: ios build
|
||||
# path: |
|
||||
# publish/ios
|
||||
51
.github/workflows/desktop.yml
vendored
51
.github/workflows/desktop.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: 'Test the desktop app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# E2E tests
|
||||
- name: Start containers
|
||||
run: docker-compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
cd submodules/btcpayserver
|
||||
nohup dotnet run -c Release --project BTCPayServer &
|
||||
while ! curl -s http://localhost:14142/api/v1/health > /dev/null; do
|
||||
echo "Waiting for BTCPay Server to start..."
|
||||
sleep 10
|
||||
done
|
||||
# Unit and integration tests
|
||||
- name: Run fast tests
|
||||
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Fast=Fast" BTCPayApp.Tests
|
||||
- name: Run integration tests
|
||||
run: dotnet test -v n --logger "console;verbosity=normal" --filter "Integration=Integration" BTCPayApp.Tests
|
||||
- name: Run Selenium tests
|
||||
run: dotnet test --filter "Selenium=Selenium" -v n --logger "console;verbosity=normal" BTCPayApp.Tests
|
||||
51
.github/workflows/maui.yml
vendored
51
.github/workflows/maui.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: 'Test the mobile app'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
# https://blog.taranissoftware.com/build-net-maui-apps-with-github-actions
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# Setup code, .NET and Android
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '22'
|
||||
distribution: 'temurin'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
# Restore and build
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
# Unit and integration tests
|
||||
- name: Run fast tests
|
||||
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Fast=Fast"
|
||||
- name: Run integration tests
|
||||
run: dotnet test --no-restore -v n --logger "console;verbosity=normal" --filter "Integration=Integration"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.idea
|
||||
**/bin
|
||||
**/obj
|
||||
**/tmp
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@ -38,6 +39,8 @@ bld/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
|
||||
17
.run/App-Server 2nd Instance.run.xml
Normal file
17
.run/App-Server 2nd Instance.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="App-Server 2nd Instance" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https-second-instance" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
17
.run/App-Server Mutinynet.run.xml
Normal file
17
.run/App-Server Mutinynet.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="App-Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BTCPayApp.Server/BTCPayApp.Server.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="mutinynet" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
18
.run/BTCPayApp.Maui.run.xml
Normal file
18
.run/BTCPayApp.Maui.run.xml
Normal file
@ -0,0 +1,18 @@
|
||||
·<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="BTCPayApp.Maui" type="XamarinAndroidProject" factoryName="Xamarin.Android">
|
||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="DEPLOY_BEHAVIOUR_NAME" value="Default" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="false" name="BTCPayApp.Maui" type="XamarinIOSProject" factoryName="Xamarin.iOS">
|
||||
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayApp.Maui/BTCPayApp.Maui.csproj" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="IS_PASS_PARENT_ENVS" value="false" />
|
||||
<option name="EXTRA_MLAUNCH_PARAMETERS" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
9
.run/DEV ALL WITH SECOND APP.run.xml
Normal file
9
.run/DEV ALL WITH SECOND APP.run.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL WITH SECOND APP" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="App-Server 2nd Instance" type="LaunchSettings" />
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
8
.run/DEV ALL testnet.run.xml
Normal file
8
.run/DEV ALL testnet.run.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL testnet" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="Server Testnet" type="LaunchSettings" />
|
||||
<toRun name="Docker compose testnet" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ALL" type="CompoundRunConfigurationType">
|
||||
<toRun name="App-Server" type="LaunchSettings" />
|
||||
<toRun name="Server" type="LaunchSettings" />
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
||||
7
.run/DEV ANDROID.run.xml
Normal file
7
.run/DEV ANDROID.run.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="DEV ANDROID" type="CompoundRunConfigurationType">
|
||||
<toRun name="Server Regtest" type="LaunchSettings" />
|
||||
<toRun name="Docker compose" type="docker-deploy" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
17
.run/Docker compose mutiny.run.xml
Normal file
17
.run/Docker compose mutiny.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Docker compose mutiny" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="services">
|
||||
<list>
|
||||
<option value="dev" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.mutinynet.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
17
.run/Docker compose testnet.run.xml
Normal file
17
.run/Docker compose testnet.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Docker compose testnet" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="services">
|
||||
<list>
|
||||
<option value="dev" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="submodules/btcpayserver/BTCPayServer.Tests/docker-compose.testnet.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
17
.run/Server Mutinynet.run.xml
Normal file
17
.run/Server Mutinynet.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Mutinynet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-MUTINYNET" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
19
.run/Server Regtest.run.xml
Normal file
19
.run/Server Regtest.run.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Regtest" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" default="false" projectName="BTCPayApp.Core" projectPath="$PROJECT_DIR$/BTCPayApp.Core/BTCPayApp.Core.csproj" />
|
||||
<option name="Build" default="false" projectName="BTCPayServer.Plugins.App" projectPath="$PROJECT_DIR$/BTCPayServer.Plugins.App/BTCPayServer.Plugins.App.csproj" />
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
17
.run/Server Testnet.run.xml
Normal file
17
.run/Server Testnet.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server Testnet" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/submodules/btcpayserver/BTCPayServer/BTCPayServer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="Bitcoin-HTTPS-TESTNET" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
26
BTCPayApp.Core/AppDatabaseMigrator.cs
Normal file
26
BTCPayApp.Core/AppDatabaseMigrator.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory) : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
|
||||
if (pendingMigrationsAsync.Length != 0)
|
||||
{
|
||||
logger.LogInformation("Applying {Length} migrations", pendingMigrationsAsync.Length);
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
logger.LogInformation("Migrations applied: {Migrations}", string.Join(", ", pendingMigrationsAsync));
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON data transfer object for the bearer token response typically found in "/login" and "/refresh" responses.
|
||||
/// </summary>
|
||||
public sealed class AccessTokenResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The value is always "Bearer" which indicates this response provides a "Bearer" token
|
||||
/// in the form of an opaque <see cref="AccessToken"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "tokenType": "Bearer" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public string TokenType { get; } = "Bearer";
|
||||
|
||||
/// <summary>
|
||||
/// The opaque bearer token to send as part of the Authorization request header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "accessToken": "{AccessToken}" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required string AccessToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds before the <see cref="AccessToken"/> expires.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "expiresIn": "{ExpiresInSeconds}" using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required long ExpiresIn { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this provides the ability to get a new access_token after it expires using a refresh endpoint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is serialized as "refreshToken": "{RefreshToken}" using using <see cref="JsonSerializerDefaults.Web"/>.
|
||||
/// </remarks>
|
||||
public required string RefreshToken { get; init; }
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The request type for the "/login" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// </summary>
|
||||
public sealed class LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's email address which acts as a user name.
|
||||
/// </summary>
|
||||
public required string Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's password.
|
||||
/// </summary>
|
||||
public required string Password { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The optional two-factor authenticator code. This may be required for users who have enabled two-factor authentication.
|
||||
/// This is not required if a <see cref="TwoFactorRecoveryCode"/> is sent.
|
||||
/// </summary>
|
||||
public string? TwoFactorCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional two-factor recovery code from <see cref="TwoFactorResponse.RecoveryCodes"/>.
|
||||
/// This is required for users who have enabled two-factor authentication but lost access to their <see cref="TwoFactorCode"/>.
|
||||
/// </summary>
|
||||
public string? TwoFactorRecoveryCode { get; init; }
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
|
||||
/// </summary>
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
|
||||
/// dereferenced, it provide human-readable documentation for the problem type
|
||||
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
|
||||
/// "about:blank".
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-5)]
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
|
||||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||
/// see[RFC7231], Section 3.4).
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-4)]
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-3)]
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable explanation specific to this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-2)]
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyOrder(-1)]
|
||||
[JsonPropertyName("instance")]
|
||||
public string? Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
|
||||
/// <para>
|
||||
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
|
||||
/// other members of a problem type.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
|
||||
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The request type for the "/refresh" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// </summary>
|
||||
public sealed class RefreshRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="AccessTokenResponse.RefreshToken"/> from the last "/login" or "/refresh" response used to get a new <see cref="AccessTokenResponse"/>
|
||||
/// with an extended expiration.
|
||||
/// </summary>
|
||||
public required string RefreshToken { get; init; }
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
namespace BTCPayApp.Core.AspNetRip;
|
||||
|
||||
/// <summary>
|
||||
/// The response type for the "/resetPassword" endpoint added by <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi"/>.
|
||||
/// The "/resetPassword" endpoint requires the "/forgotPassword" endpoint to be called first to get the <see cref="ResetCode"/>.
|
||||
/// </summary>
|
||||
public sealed class ResetPasswordRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The email address for the user requesting a password reset. This should match <see cref="ForgotPasswordRequest.Email"/>.
|
||||
/// </summary>
|
||||
public required string Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The code sent to the user's email to reset the password. To get the reset code, first make a "/forgotPassword" request.
|
||||
/// </summary>
|
||||
public required string ResetCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The new password the user with the given <see cref="Email"/> should login with. This will replace the previous password.
|
||||
/// </summary>
|
||||
public required string NewPassword { get; init; }
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
using System.Text;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using LightningPayment = BTCPayApp.CommonServer.Models.LightningPayment;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> logger, IServiceProvider serviceProvider) : IBTCPayAppHubClient
|
||||
{
|
||||
public event AsyncEventHandler<string>? OnNewBlock;
|
||||
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
|
||||
public event AsyncEventHandler<string>? OnNotifyNetwork;
|
||||
public event AsyncEventHandler<string>? OnServerNodeInfo;
|
||||
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
|
||||
|
||||
public async Task NotifyServerEvent(ServerEvent serverEvent)
|
||||
{
|
||||
logger.LogInformation("NotifyServerEvent: {Type} - {Details}", serverEvent.Type, serverEvent.ToString());
|
||||
await OnNotifyServerEvent?.Invoke(this, serverEvent)!;
|
||||
}
|
||||
|
||||
public async Task NotifyNetwork(string network)
|
||||
{
|
||||
logger.LogInformation("NotifyNetwork: {network}", network);
|
||||
await OnNotifyNetwork?.Invoke(this, network);
|
||||
}
|
||||
|
||||
public async Task NotifyServerNode(string nodeInfo)
|
||||
{
|
||||
logger.LogInformation("NotifyServerNode: {nodeInfo}", nodeInfo);
|
||||
await OnServerNodeInfo?.Invoke(this, nodeInfo);
|
||||
}
|
||||
|
||||
public async Task TransactionDetected(TransactionDetectedRequest request)
|
||||
{
|
||||
logger.LogInformation($"OnTransactionDetected: {request.TxId}");
|
||||
await OnTransactionDetected?.Invoke(this, request);
|
||||
}
|
||||
|
||||
public async Task NewBlock(string block)
|
||||
{
|
||||
logger.LogInformation("NewBlock: {block}", block);
|
||||
await OnNewBlock?.Invoke(this, block);
|
||||
}
|
||||
|
||||
private PaymentsManager PaymentsManager =>
|
||||
serviceProvider.GetRequiredService<LightningNodeManager>().Node.PaymentsManager;
|
||||
|
||||
public async Task<LightningPayment> CreateInvoice(CreateLightningInvoiceRequest createLightningInvoiceRequest)
|
||||
{
|
||||
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
|
||||
false);
|
||||
return await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
|
||||
createLightningInvoiceRequest.Expiry, descHash);
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningInvoice(string paymentHash)
|
||||
{
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invs.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningPayment(string paymentHash)
|
||||
{
|
||||
var invs = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invs.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<List<LightningPayment>> GetLightningPayments(ListPaymentsParams request)
|
||||
{
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound), default);
|
||||
}
|
||||
|
||||
public async Task<List<LightningPayment>> GetLightningInvoices(ListInvoicesParams request)
|
||||
{
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound), default);
|
||||
}
|
||||
|
||||
public async Task<PayResponse> PayInvoice(string bolt11, long? amountMilliSatoshi)
|
||||
{
|
||||
var network = serviceProvider.GetRequiredService<OnChainWalletManager>().Network;
|
||||
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
|
||||
try
|
||||
{
|
||||
var result = await PaymentsManager.PayInvoice(bolt,
|
||||
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
|
||||
return new PayResponse()
|
||||
{
|
||||
Result = result.Status switch
|
||||
{
|
||||
LightningPaymentStatus.Unknown => PayResult.Unknown,
|
||||
LightningPaymentStatus.Pending => PayResult.Unknown,
|
||||
LightningPaymentStatus.Complete => PayResult.Ok,
|
||||
LightningPaymentStatus.Failed => PayResult.Error,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Details = new PayDetails()
|
||||
{
|
||||
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
|
||||
Status = result.Status
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error paying invoice");
|
||||
return new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
using System.Net;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayConnectionManager : IHostedService, IHubConnectionObserver
|
||||
{
|
||||
private readonly IAccountManager _accountManager;
|
||||
private readonly AuthenticationStateProvider _authStateProvider;
|
||||
private readonly ILogger<BTCPayConnectionManager> _logger;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly IBTCPayAppHubClient _btcPayAppServerClientInterface;
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public IBTCPayAppHubServer? HubProxy { get; private set; }
|
||||
public HubConnection? Connection { get; private set; }
|
||||
public Network? ReportedNetwork { get; private set; }
|
||||
|
||||
public string ReportedNodeInfo { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(HubConnectionState Old, HubConnectionState New)>? ConnectionChanged;
|
||||
private HubConnectionState _connectionState = HubConnectionState.Disconnected;
|
||||
|
||||
public HubConnectionState ConnectionState
|
||||
{
|
||||
get => Connection?.State ?? HubConnectionState.Disconnected;
|
||||
private set
|
||||
{
|
||||
if (_connectionState == value)
|
||||
return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
_logger.LogInformation("Connection state changed: {State}", _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayConnectionManager(
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface)
|
||||
{
|
||||
_accountManager = accountManager;
|
||||
_authStateProvider = authStateProvider;
|
||||
_logger = logger;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_btcPayAppServerClientInterface = btcPayAppServerClientInterface;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
_btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
|
||||
_btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
|
||||
await StartOrReplace();
|
||||
_ = TryStayConnected();
|
||||
}
|
||||
|
||||
private async Task OnServerNodeInfo(object? sender, string e)
|
||||
{
|
||||
ReportedNodeInfo = e;
|
||||
}
|
||||
|
||||
private async Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
_logger.LogInformation("OnNotifyServerEvent: {Type} - {Details}", e.Type, e.ToString());
|
||||
}
|
||||
|
||||
private async Task OnNotifyNetwork(object? sender, string e)
|
||||
{
|
||||
ReportedNetwork = Network.GetNetwork(e);
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
var authenticated = await _accountManager.CheckAuthenticated();
|
||||
if (!authenticated)
|
||||
await Kill();
|
||||
else
|
||||
await StartOrReplace();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while handling authentication state change");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryStayConnected()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connection is not null && ConnectionState == HubConnectionState.Disconnected)
|
||||
{
|
||||
await Connection.StartAsync();
|
||||
ConnectionState = HubConnectionState.Connected;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
var result = await _accountManager.RefreshAccess();
|
||||
if (result.Succeeded)
|
||||
await StartOrReplace();
|
||||
else
|
||||
await Kill();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Kill()
|
||||
{
|
||||
if (Connection is not null)
|
||||
await Connection.StopAsync();
|
||||
Connection = null;
|
||||
ConnectionState = HubConnectionState.Disconnected;
|
||||
_subscription?.Dispose();
|
||||
HubProxy = null;
|
||||
}
|
||||
|
||||
private async Task StartOrReplace()
|
||||
{
|
||||
await Kill();
|
||||
var account = _accountManager.GetAccount();
|
||||
if (account is null)
|
||||
return;
|
||||
Connection = new HubConnectionBuilder()
|
||||
.AddNewtonsoftJsonProtocol()
|
||||
.WithUrl(new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString(), options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => Task.FromResult(_accountManager.GetAccount()?.AccessToken);
|
||||
})
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_subscription = Connection.Register(_btcPayAppServerClientInterface);
|
||||
HubProxy = Connection.CreateHubProxy<IBTCPayAppHubServer>();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
_btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnClosed(Exception? exception)
|
||||
{
|
||||
_logger.LogError(exception, "Hub connection closed");
|
||||
ConnectionState = HubConnectionState.Disconnected;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnected(string? connectionId)
|
||||
{
|
||||
_logger.LogInformation("Hub reconnected: {ConnectionId}", connectionId);
|
||||
ConnectionState = HubConnectionState.Connected;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnecting(Exception? exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Hub reconnecting");
|
||||
ConnectionState = HubConnectionState.Connecting;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class BTCPayPaymentsNotifier : IScopedHostedService
|
||||
{
|
||||
private readonly PaymentsManager _paymentsManager;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager, BTCPayConnectionManager connectionManager,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_paymentsManager = paymentsManager;
|
||||
_connectionManager = connectionManager;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
}
|
||||
|
||||
private async Task OnPaymentUpdate(object? sender, LightningPayment e)
|
||||
{
|
||||
await _connectionManager.HubProxy
|
||||
.SendPaymentUpdate(
|
||||
_onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier, e)
|
||||
.RunSync();
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LDKChangeDestinationSource:ChangeDestinationSourceInterface
|
||||
{
|
||||
private readonly LightningNodeManager _lightningNodeManager;
|
||||
|
||||
public LDKChangeDestinationSource( LightningNodeManager lightningNodeManager)
|
||||
{
|
||||
_lightningNodeManager = lightningNodeManager;
|
||||
}
|
||||
public Result_CVec_u8ZNoneZ get_change_destination_script()
|
||||
{
|
||||
var s = _lightningNodeManager.Node.DeriveScript().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_u8ZNoneZ.ok(s.ToBytes());
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using org.ldk.enums;
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LDKKVStore:KVStoreInterface
|
||||
{
|
||||
private readonly IConfigProvider _configProvider;
|
||||
|
||||
public LDKKVStore(IConfigProvider configProvider)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
}
|
||||
|
||||
public Result_CVec_u8ZIOErrorZ read(string primary_namespace, string secondary_namespace, string key)
|
||||
{
|
||||
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
|
||||
var result = _configProvider.Get<byte[]>(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return result == null ? Result_CVec_u8ZIOErrorZ.err(IOError.LDKIOError_NotFound) : Result_CVec_u8ZIOErrorZ.ok(result);
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ write(string primary_namespace, string secondary_namespace, string key, byte[] buf)
|
||||
{
|
||||
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
|
||||
_configProvider.Set(key1, buf).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_NoneIOErrorZ remove(string primary_namespace, string secondary_namespace, string key, bool lazy)
|
||||
{
|
||||
var key1 = $"{primary_namespace}:{secondary_namespace}:{key}";
|
||||
_configProvider.Set<byte[]>(key1, null).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_NoneIOErrorZ.ok();
|
||||
}
|
||||
|
||||
public Result_CVec_StrZIOErrorZ list(string primary_namespace, string secondary_namespace)
|
||||
{
|
||||
var key1 = $"{primary_namespace}:{secondary_namespace}:";
|
||||
var result = _configProvider.List(key1).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return Result_CVec_StrZIOErrorZ.ok(result.ToArray());
|
||||
}
|
||||
}
|
||||
@ -1,384 +0,0 @@
|
||||
using System.Collections.Specialized;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using org.ldk.structs;
|
||||
using OutPoint = NBitcoin.OutPoint;
|
||||
using UInt128 = org.ldk.util.UInt128;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public partial class LDKNode:
|
||||
ILDKEventHandler<Event.Event_ChannelClosed>,
|
||||
ILDKEventHandler<Event.Event_ChannelPending>,
|
||||
ILDKEventHandler<Event.Event_ChannelReady>
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<ChannelDetails[]> GetChannels(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(nameof(GetChannels), async entry =>
|
||||
{
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
return ServiceProvider.GetRequiredService<ChannelManager>().list_channels();
|
||||
}).WithCancellation(cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task Handle(Event.Event_ChannelClosed evt)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelPending @event)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task Handle(Event.Event_ChannelReady @event)
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task<PeerDetails[]> GetPeers(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
return await _memoryCache.GetOrCreateAsync(nameof(GetPeers), async entry =>
|
||||
{
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
return ServiceProvider.GetRequiredService<PeerManager>().list_peers();
|
||||
}).WithCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public void PeersChanged()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
}
|
||||
|
||||
private void InvalidateCache()
|
||||
{
|
||||
_memoryCache.Remove(nameof(GetPeers));
|
||||
_memoryCache.Remove(nameof(GetChannels));
|
||||
}
|
||||
|
||||
public async Task<Result_ChannelIdAPIErrorZ> OpenChannel(Money amount, PubKey nodeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
|
||||
var channelManager = ServiceProvider.GetRequiredService<ChannelManager>();
|
||||
var entropySource = ServiceProvider.GetRequiredService<EntropySource>();
|
||||
var userConfig = ServiceProvider.GetRequiredService<UserConfig>();
|
||||
|
||||
|
||||
var temporaryChannelId = ChannelId.temporary_from_entropy_source(entropySource);
|
||||
|
||||
|
||||
var userChannelId = new UInt128(temporaryChannelId.get_a().Take(16).ToArray());
|
||||
try
|
||||
{
|
||||
return await Task.Run(() => channelManager.create_channel(nodeId.ToBytes(), amount.Satoshi, 0, userChannelId,
|
||||
temporaryChannelId, userConfig), cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
_logger.LogInformation("finished (trying to) opening channel with {nodeId} for {amount}", nodeId, amount);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public partial class LDKNode : IAsyncDisposable, IHostedService, IDisposable
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly BTCPayConnectionManager _connectionManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
|
||||
public LDKNode(
|
||||
IMemoryCache cache,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
BTCPayConnectionManager connectionManager,
|
||||
IServiceProvider serviceProvider,
|
||||
LDKWalletLogger logger,
|
||||
IConfigProvider configProvider,
|
||||
OnChainWalletManager onChainWalletManager)
|
||||
{
|
||||
_memoryCache = cache;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_connectionManager = connectionManager;
|
||||
_logger = logger;
|
||||
_configProvider = configProvider;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private TaskCompletionSource? _started;
|
||||
private readonly SemaphoreSlim _semaphore = new(1);
|
||||
|
||||
public Network Network => ServiceProvider.GetRequiredService<Network>();
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
bool exists;
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
exists = _started is not null;
|
||||
_started ??= new TaskCompletionSource();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
if (exists)
|
||||
{
|
||||
await _started.Task;
|
||||
return;
|
||||
}
|
||||
InvalidateCache();
|
||||
_config = await _configProvider.Get<LightningConfig>(key: LightningConfig.Key)?? new LightningConfig();
|
||||
_configLoaded.SetResult();
|
||||
var keyPath = KeyPath.Parse(_config.LightningDerivationPath);
|
||||
Seed = new Mnemonic( _onChainWalletManager.WalletConfig.Mnemonic).DeriveExtKey().Derive(keyPath).PrivateKey.ToBytes();
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
|
||||
_logger.LogInformation("Starting LDKNode services");
|
||||
var bb = await _onChainWalletManager.GetBestBlock();
|
||||
if (bb is null)
|
||||
{
|
||||
throw new InvalidOperationException("Best block could not be retrieved. Killing the startup");
|
||||
}
|
||||
foreach (var service in services)
|
||||
{
|
||||
_logger.LogInformation($"Starting {service.GetType().Name}");
|
||||
await service.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_started.SetResult();
|
||||
_logger.LogInformation("LDKNode started");
|
||||
}
|
||||
|
||||
private readonly TaskCompletionSource _configLoaded = new();
|
||||
|
||||
public async Task<LightningConfig> GetConfig()
|
||||
{
|
||||
await _configLoaded.Task;
|
||||
return _config!;
|
||||
}
|
||||
|
||||
private async Task UpdateConfig(LightningConfig config)
|
||||
{
|
||||
await _started.Task;
|
||||
await _configProvider.Set(LightningConfig.Key, config);
|
||||
_config = config;
|
||||
ConfigUpdated?.Invoke(this, config);
|
||||
}
|
||||
|
||||
|
||||
public AsyncEventHandler<LightningConfig>? ConfigUpdated;
|
||||
|
||||
public byte[] Seed { get; private set; }
|
||||
|
||||
public PaymentsManager PaymentsManager => ServiceProvider.GetRequiredService<PaymentsManager>();
|
||||
public LDKPeerHandler PeerHandler => ServiceProvider.GetRequiredService<LDKPeerHandler>();
|
||||
|
||||
public PubKey NodeId => new(ServiceProvider.GetRequiredService<ChannelManager>().get_our_node_id());
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
bool exists;
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
exists = _started is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
if (!exists)
|
||||
return;
|
||||
var identifier = _onChainWalletManager.WalletConfig.Derivations[WalletDerivation.LightningScripts].Identifier;
|
||||
|
||||
|
||||
_logger.LogInformation("Stopping LDKNode services");
|
||||
var services = ServiceProvider.GetServices<IScopedHostedService>();
|
||||
var tasks = services.Select(async service =>
|
||||
{
|
||||
_logger.LogInformation($"Stopping {service.GetType().Name}");
|
||||
await service.StopAsync(cancellationToken);
|
||||
_logger.LogInformation($"Stopped {service.GetType().Name}");
|
||||
}).ToArray();
|
||||
await Task.WhenAll(tasks);
|
||||
_ = _connectionManager.HubProxy.IdentifierActive(identifier, false).RunSync();
|
||||
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeAsync().GetAwaiter().GetResult();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// await StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private readonly TaskCompletionSource<ChannelMonitor[]?> icm = new();
|
||||
private LightningConfig? _config;
|
||||
|
||||
public async Task<ChannelMonitor[]> GetInitialChannelMonitors()
|
||||
{
|
||||
return await icm.Task;
|
||||
}
|
||||
private async Task<ChannelMonitor[]> GetInitialChannelMonitors(EntropySource entropySource,
|
||||
SignerProvider signerProvider)
|
||||
{
|
||||
await using var db = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var data = await db.LightningChannels.Select(channel => channel.Data)
|
||||
.ToArrayAsync();
|
||||
|
||||
var channels = ChannelManagerHelper.GetInitialMonitors(data, entropySource, signerProvider);
|
||||
icm.SetResult(channels);
|
||||
return channels;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<byte[]?> GetRawChannelManager()
|
||||
{
|
||||
return await _configProvider.Get<byte[]>("ChannelManager") ?? null;
|
||||
}
|
||||
|
||||
public async Task UpdateChannelManager(ChannelManager serializedChannelManager)
|
||||
{
|
||||
await _configProvider.Set("ChannelManager", serializedChannelManager.write());
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateNetworkGraph(NetworkGraph networkGraph)
|
||||
{
|
||||
await _configProvider.Set("NetworkGraph", networkGraph.write());
|
||||
}
|
||||
|
||||
public async Task UpdateScore(WriteableScore score)
|
||||
{
|
||||
await _configProvider.Set("Score", score.write());
|
||||
}
|
||||
|
||||
|
||||
public async Task<(byte[] serializedChannelManager, ChannelMonitor[] channelMonitors)?> GetSerializedChannelManager(
|
||||
EntropySource entropySource, SignerProvider signerProvider)
|
||||
{
|
||||
|
||||
var data = await GetRawChannelManager();
|
||||
if (data is null)
|
||||
{
|
||||
icm.SetResult(Array.Empty<ChannelMonitor>());
|
||||
return null;
|
||||
}
|
||||
|
||||
var channels = await GetInitialChannelMonitors(entropySource, signerProvider);
|
||||
return (data, channels);
|
||||
}
|
||||
|
||||
public async Task<Script> DeriveScript()
|
||||
{
|
||||
var derivationKey = (await GetConfig()).ScriptDerivationKey;
|
||||
return await _onChainWalletManager.DeriveScript(derivationKey);
|
||||
}
|
||||
|
||||
|
||||
public async Task TrackScripts(Script[] scripts, string derivation = WalletDerivation.LightningScripts)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
_logger.LogDebug("Tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
var identifier = _onChainWalletManager.WalletConfig.Derivations[derivation].Identifier;
|
||||
|
||||
await _connectionManager.HubProxy.TrackScripts(identifier,
|
||||
scripts.Select(script => script.ToHex()).ToArray()).RunSync();
|
||||
_logger.LogDebug("Tracked scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error tracking scripts {scripts}", string.Join(",", scripts.Select(script => script.ToHex())));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateChannel(string id, byte[] write)
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var channel = await context.LightningChannels.SingleOrDefaultAsync(lightningChannel => lightningChannel.Id == id || lightningChannel.Aliases.Contains(id));
|
||||
|
||||
if (channel is not null)
|
||||
{
|
||||
if (!channel.Aliases.Contains(channel.Id))
|
||||
{
|
||||
channel.Aliases.Add(channel.Id);
|
||||
}
|
||||
if (!channel.Aliases.Contains(id))
|
||||
{
|
||||
channel.Aliases.Add(id);
|
||||
}
|
||||
|
||||
channel.Id = id;
|
||||
channel.Data = write;
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.LightningChannels.AddAsync(new Channel()
|
||||
{
|
||||
Id = id,
|
||||
Data = write,
|
||||
Aliases = [id]
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task Peer(string toString, PeerInfo? value)
|
||||
{
|
||||
toString = toString.ToLowerInvariant();
|
||||
var config = await GetConfig();
|
||||
if (value is null)
|
||||
{
|
||||
if (config.Peers.Remove(toString))
|
||||
{
|
||||
await UpdateConfig(config);
|
||||
return;
|
||||
}
|
||||
}
|
||||
config.Peers.AddOrReplace(toString, value);
|
||||
await UpdateConfig(config);
|
||||
}
|
||||
}
|
||||
@ -1,266 +0,0 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LightningNodeManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-LN";
|
||||
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<LightningNodeManager> _logger;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
private IServiceScope? _nodeScope;
|
||||
public LDKNode? Node => _nodeScope?.ServiceProvider.GetService<LDKNode>();
|
||||
private LightningNodeState _state = LightningNodeState.Init;
|
||||
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
|
||||
private bool IsOnchainConfigured => _onChainWalletManager.WalletConfig is not null;
|
||||
private bool IsOnchainLightningDerivationConfigured => _onChainWalletManager.WalletConfig?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
|
||||
public bool CanConfigureLightningNode => IsHubConnected && IsOnchainConfigured && !IsOnchainLightningDerivationConfigured && State == LightningNodeState.NotConfigured;
|
||||
public string? ConnectionString => IsOnchainLightningDerivationConfigured
|
||||
? $"type=app;group={_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower()
|
||||
: null;
|
||||
|
||||
public LightningNodeState State
|
||||
{
|
||||
get => _state;
|
||||
private set
|
||||
{
|
||||
if (_state == value)
|
||||
return;
|
||||
var old = _state;
|
||||
_state = value;
|
||||
_logger.LogInformation("Lightning node state changed: {State}", _state);
|
||||
StateChanged?.Invoke(this, (old, value));
|
||||
}
|
||||
}
|
||||
|
||||
public event AsyncEventHandler<(LightningNodeState Old, LightningNodeState New)>? StateChanged;
|
||||
|
||||
public LightningNodeManager(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<LightningNodeManager> logger,
|
||||
OnChainWalletManager onChainWalletManager,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_onChainWalletManager = onChainWalletManager;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task StartNode()
|
||||
{
|
||||
if (_nodeScope is not null || State is LightningNodeState.Loaded)
|
||||
return;
|
||||
await _controlSemaphore.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (_nodeScope is null)
|
||||
{
|
||||
_nodeScope = _serviceScopeFactory.CreateScope();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
await Node.StartAsync(_cancellationTokenSource.Token);
|
||||
|
||||
State = LightningNodeState.Loaded;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_nodeScope.Dispose();
|
||||
_logger.LogError(e, "Error while starting lightning node");
|
||||
_nodeScope = null;
|
||||
State = LightningNodeState.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopNode()
|
||||
{
|
||||
if (_nodeScope is null || State is not LightningNodeState.Loaded)
|
||||
return;
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token);
|
||||
cts.CancelAfter(5000);
|
||||
await Node.StopAsync(cts.Token);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while stopping lightning node");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nodeScope?.Dispose();
|
||||
_nodeScope = null;
|
||||
_controlSemaphore.Release();
|
||||
State = LightningNodeState.Stopped;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanseTask()
|
||||
{
|
||||
await StopNode();
|
||||
|
||||
if (_nodeScope is not null || State == LightningNodeState.NotConfigured) return;
|
||||
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _onChainWalletManager.RemoveDerivation(WalletDerivation.LightningScripts);
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
context.LightningPayments.RemoveRange(context.LightningPayments);
|
||||
context.LightningChannels.RemoveRange(context.LightningChannels);
|
||||
context.Settings.RemoveRange(context.Settings.Where(s => new string[]{"ChannelManager","NetworkGraph","Score","lightningconfig"}.Contains(s.Key)));
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
|
||||
State = LightningNodeState.NotConfigured;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Generate()
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != LightningNodeState.NotConfigured) return;
|
||||
if (!IsHubConnected)
|
||||
throw new InvalidOperationException("Cannot configure lightning node without BTCPay connection");
|
||||
if (!IsOnchainConfigured)
|
||||
throw new InvalidOperationException("Cannot configure lightning node without on-chain wallet configuration");
|
||||
if (IsOnchainLightningDerivationConfigured)
|
||||
throw new InvalidOperationException("On-chain wallet is already configured with a lightning derivation");
|
||||
|
||||
await _onChainWalletManager.AddDerivation(WalletDerivation.LightningScripts, "Lightning", null);
|
||||
// await _onChainWalletManager.AddDerivation(WalletDerivation.SpendableOutputs, "Lightning Spendables", null);
|
||||
State = LightningNodeState.WaitingForConnection;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) state)
|
||||
{
|
||||
if (IsHubConnected && State == LightningNodeState.WaitingForConnection)
|
||||
{
|
||||
State = LightningNodeState.Loading;
|
||||
}
|
||||
else if (_btcPayConnectionManager.ConnectionState == HubConnectionState.Disconnected && State is LightningNodeState.Loading or LightningNodeState.Loaded)
|
||||
{
|
||||
_ = StopNode();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnChainWalletManagerOnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
|
||||
{
|
||||
if (e.New == OnChainWalletState.Loaded)
|
||||
{
|
||||
State = LightningNodeState.Loading;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnStateChanged(object? sender, (LightningNodeState Old, LightningNodeState New) state)
|
||||
{
|
||||
LightningNodeState? newState = null;
|
||||
try
|
||||
{
|
||||
switch (state.New)
|
||||
{
|
||||
case LightningNodeState.WaitingForConnection:
|
||||
{
|
||||
if (IsHubConnected)
|
||||
newState = LightningNodeState.Loading;
|
||||
break;
|
||||
}
|
||||
case LightningNodeState.Loading:
|
||||
if (!IsHubConnected)
|
||||
{
|
||||
newState = LightningNodeState.WaitingForConnection;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!IsOnchainConfigured || !IsOnchainLightningDerivationConfigured)
|
||||
{
|
||||
newState = LightningNodeState.NotConfigured;
|
||||
break;
|
||||
}
|
||||
|
||||
var result = await _btcPayConnectionManager.HubProxy!
|
||||
.IdentifierActive(_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier, true)
|
||||
.RunSync();
|
||||
if (result)
|
||||
{
|
||||
await StartNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Introduce a new state so that this node knows that another instance is active
|
||||
newState = LightningNodeState.Error;
|
||||
}
|
||||
break;
|
||||
|
||||
case LightningNodeState.NotConfigured:
|
||||
if (CanConfigureLightningNode)
|
||||
{
|
||||
await Generate();
|
||||
}
|
||||
break;
|
||||
|
||||
case LightningNodeState.Loaded:
|
||||
await _controlSemaphore.WaitAsync();
|
||||
|
||||
_controlSemaphore.Release();
|
||||
break;
|
||||
// case LightningNodeState.Unloading:
|
||||
// _nodeScope?.Dispose();
|
||||
// State = _walletConfig is null
|
||||
// ? LightningNodeState.NotConfigured
|
||||
// : LightningNodeState.WaitingForConnection;
|
||||
// break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (newState is not null)
|
||||
State = newState.Value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
State = LightningNodeState.Init;
|
||||
StateChanged += OnStateChanged;
|
||||
_btcPayConnectionManager.ConnectionChanged += OnConnectionChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_btcPayConnectionManager.ConnectionChanged -= OnConnectionChanged;
|
||||
_onChainWalletManager.StateChanged += OnChainWalletManagerOnStateChanged;
|
||||
StateChanged -= OnStateChanged;
|
||||
_nodeScope?.Dispose();
|
||||
}
|
||||
}
|
||||
@ -1,515 +0,0 @@
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Scripting;
|
||||
using OutPoint = NBitcoin.OutPoint;
|
||||
using TxOut = NBitcoin.TxOut;
|
||||
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
public class OnChainWalletManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-CHAIN";
|
||||
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
private readonly ILogger<OnChainWalletManager> _logger;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private OnChainWalletState _state = OnChainWalletState.Init;
|
||||
|
||||
public WalletConfig? WalletConfig { get; private set; }
|
||||
public Network? Network => WalletConfig is null ? null : Network.GetNetwork(WalletConfig.Network);
|
||||
|
||||
public OnChainWalletState State
|
||||
{
|
||||
get => _state;
|
||||
private set
|
||||
{
|
||||
if (_state == value)
|
||||
return;
|
||||
var old = _state;
|
||||
_state = value;
|
||||
_logger.LogInformation("Wallet state changed: {State}", _state);
|
||||
StateChanged?.Invoke(this, (old, value));
|
||||
}
|
||||
}
|
||||
|
||||
public event AsyncEventHandler<(OnChainWalletState Old, OnChainWalletState New)>? StateChanged;
|
||||
|
||||
public OnChainWalletManager(
|
||||
IConfigProvider configProvider,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
BTCPayConnectionManager btcPayConnectionManager,
|
||||
ILogger<OnChainWalletManager> logger,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_configProvider = configProvider;
|
||||
_btcPayAppServerClient = btcPayAppServerClient;
|
||||
_btcPayConnectionManager = btcPayConnectionManager;
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
StateChanged += OnStateChanged;
|
||||
_btcPayAppServerClient.OnNewBlock += OnNewBlock;
|
||||
_btcPayAppServerClient.OnTransactionDetected += OnTransactionDetected;
|
||||
_btcPayConnectionManager.ConnectionChanged += ConnectionChanged;
|
||||
WalletConfig = await _configProvider.Get<WalletConfig>(WalletConfig.Key);
|
||||
DetermineState();
|
||||
if (IsHubConnected)
|
||||
{
|
||||
await Track();
|
||||
|
||||
_ = GetBestBlock();
|
||||
State = OnChainWalletState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
|
||||
public bool IsConfigured => WalletConfig is not null;
|
||||
|
||||
private async Task OnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
|
||||
{
|
||||
if (e is { Old: OnChainWalletState.NotConfigured or OnChainWalletState.WaitingForConnection } && IsHubConnected && !IsConfigured)
|
||||
{
|
||||
await Generate();
|
||||
}
|
||||
|
||||
if (e is {New: OnChainWalletState.Loaded} && IsConfigured)
|
||||
{
|
||||
await Track();
|
||||
}
|
||||
|
||||
if (e.New is OnChainWalletState.Loading)
|
||||
{
|
||||
DetermineState();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Generate()
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.NotConfigured || IsConfigured || !IsHubConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot generate wallet in current state");
|
||||
}
|
||||
|
||||
var mnemonic = new Mnemonic(Wordlist.English, WordCount.Twelve);
|
||||
var mainnet = _btcPayConnectionManager.ReportedNetwork == Network.Main;
|
||||
var path = new KeyPath($"m/84'/{(mainnet ? "0" : "1")}'/0'");
|
||||
var fingerprint = mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint();
|
||||
var xpub = mnemonic.DeriveExtKey().Derive(path).Neuter().ToString(_btcPayConnectionManager.ReportedNetwork);
|
||||
var walletConfig = new WalletConfig
|
||||
{
|
||||
Mnemonic = mnemonic.ToString(),
|
||||
Network = _btcPayConnectionManager.ReportedNetwork.ToString(),
|
||||
Derivations = new Dictionary<string, WalletDerivation>()
|
||||
{
|
||||
[WalletDerivation.NativeSegwit] = new WalletDerivation()
|
||||
{
|
||||
Name = "Native Segwit",
|
||||
Descriptor = OutputDescriptor.AddChecksum(
|
||||
$"wpkh([{fingerprint.ToString()}/{path}]{xpub}/0/*)")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest()
|
||||
{
|
||||
Derivations = walletConfig.Derivations.ToDictionary(pair => pair.Key, pair => pair.Value.Descriptor)
|
||||
}).RunSync();
|
||||
foreach (var keyValuePair in result)
|
||||
{
|
||||
walletConfig.Derivations[keyValuePair.Key].Identifier = keyValuePair.Value;
|
||||
|
||||
}
|
||||
await _configProvider.Set(WalletConfig.Key, walletConfig);
|
||||
WalletConfig = walletConfig;
|
||||
State = OnChainWalletState.Loaded;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddDerivation(string key, string name, string? descriptor)
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.Loaded || !IsConfigured || !IsHubConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add deriv in current state");
|
||||
}
|
||||
if (WalletConfig.Derivations.ContainsKey(key))
|
||||
throw new InvalidOperationException("Derivation already exists");
|
||||
|
||||
var result = await _btcPayConnectionManager.HubProxy.Pair(new PairRequest
|
||||
{
|
||||
Derivations = new Dictionary<string, string?>()
|
||||
{
|
||||
[key] = descriptor
|
||||
}
|
||||
}).RunSync();
|
||||
WalletConfig.Derivations[key] = new WalletDerivation()
|
||||
{
|
||||
Name = name,
|
||||
Descriptor = descriptor,
|
||||
Identifier = result[key]
|
||||
};
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectionChanged(object? sender, (HubConnectionState Old, HubConnectionState New) _)
|
||||
{
|
||||
DetermineState();
|
||||
}
|
||||
|
||||
private void DetermineState()
|
||||
{
|
||||
if (IsHubConnected && IsConfigured)
|
||||
State = OnChainWalletState.Loaded;
|
||||
else if (!IsHubConnected)
|
||||
State = OnChainWalletState.WaitingForConnection;
|
||||
else if (!IsConfigured)
|
||||
State = OnChainWalletState.NotConfigured;
|
||||
}
|
||||
|
||||
private async Task Track()
|
||||
{
|
||||
if (!IsConfigured || !IsHubConnected)
|
||||
return;
|
||||
|
||||
var identifiers = WalletConfig.Derivations.Select(pair => pair.Value.Identifier).ToArray();
|
||||
var response = await _btcPayConnectionManager.HubProxy.Handshake(new AppHandshake
|
||||
{
|
||||
Identifiers = identifiers
|
||||
}).RunSync();
|
||||
|
||||
var missing =
|
||||
WalletConfig.Derivations.Where(pair => !response.IdentifiersAcknowledged.Contains(pair.Value.Identifier));
|
||||
|
||||
if (missing.Any())
|
||||
{
|
||||
_logger.LogWarning("Some identifiers that we had asked for BtcPayServer to track were not confirmed as being listened to. Tracking will be incomplete and functionality will critically fail.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_btcPayAppServerClient.OnNewBlock -= OnNewBlock;
|
||||
_btcPayAppServerClient.OnTransactionDetected -= OnTransactionDetected;
|
||||
_btcPayConnectionManager.ConnectionChanged -= ConnectionChanged;
|
||||
WalletConfig = null;
|
||||
State = OnChainWalletState.Init;
|
||||
}
|
||||
|
||||
private async Task OnTransactionDetected(object? sender, TransactionDetectedRequest transactionDetectedRequest)
|
||||
{
|
||||
}
|
||||
|
||||
private async Task OnNewBlock(object? sender, string e)
|
||||
{
|
||||
_memoryCache.Remove("bestblock");
|
||||
_ = GetBestBlock();
|
||||
}
|
||||
|
||||
public async Task<Script> DeriveScript(string derivation)
|
||||
{
|
||||
var identifier = WalletConfig?.Derivations[derivation].Identifier;
|
||||
var addr = await _btcPayConnectionManager.HubProxy.DeriveScript(identifier).RunSync();
|
||||
return Script.FromHex(addr);
|
||||
}
|
||||
|
||||
public async Task<byte[]?> SignTransaction(byte[] psbtBytes)
|
||||
{
|
||||
var psbt = PSBT.Load(psbtBytes, Network);
|
||||
psbt = await SignTransaction(psbt);
|
||||
return psbt?.ToBytes();
|
||||
}
|
||||
public async Task<PSBT?> SignTransaction(PSBT psbt)
|
||||
{
|
||||
var identifiers = WalletConfig.Derivations.Select(derivation => derivation.Value.Identifier).ToArray();
|
||||
var updated = await _btcPayConnectionManager.HubProxy.UpdatePsbt(identifiers, psbt.ToHex()).RunSync();
|
||||
psbt = PSBT.Parse(updated, Network);
|
||||
var rootKey =new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey();
|
||||
foreach (var deriv in WalletConfig.Derivations.Values.Where(derivation => derivation.Descriptor is not null))
|
||||
{
|
||||
var data = deriv.Descriptor.ExtractFromDescriptor(Network);
|
||||
if(data is null)
|
||||
continue;
|
||||
var accKey = rootKey.Derive(data.Value.Item2);
|
||||
psbt = psbt.SignAll(data.Value.Item1.AsHDScriptPubKey(data.Value.Item3), accKey);
|
||||
if(psbt.TryFinalize(out _))
|
||||
break;
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private static ICoin ToCoin(CoinResponse response)
|
||||
{
|
||||
var outpoint = OutPoint.Parse(response.Outpoint);
|
||||
var scriptPubKey = Script.FromHex(response.Script);
|
||||
var amount = Money.Coins(response.Value);
|
||||
return new Coin(outpoint, new TxOut(amount, scriptPubKey));
|
||||
}
|
||||
|
||||
// // public class SpendableOutputDescriptorCoin : Coin,ISignableCoin
|
||||
// {
|
||||
// public SpendableOutputDescriptorCoin(OutPoint fromOutpoint, TxOut fromTxOut, SpendableOutputDescriptor descriptor) : base(fromOutpoint, fromTxOut)
|
||||
// {
|
||||
// Descriptor = descriptor;
|
||||
// }
|
||||
//
|
||||
// public SpendableOutputDescriptor Descriptor { get;}
|
||||
// public async Task<PSBT> Sign(PSBT psbt)
|
||||
// {
|
||||
//
|
||||
// UtilMethods.
|
||||
// UtilMethods.SpendableOutputDescriptor_create_spendable_outputs_psbt(new SpendableOutputDescriptor[]{Descriptor}, )
|
||||
// Descriptor.create_spendable_outpcreate_spendable_outputs_psbtuts_psbt
|
||||
// switch (Descriptor)
|
||||
// {
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_DelayedPaymentOutput spendableOutputDescriptorDelayedPaymentOutput:
|
||||
// spendableOutputDescriptorDelayedPaymentOutput.delayed_payment_output.
|
||||
// break;
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticOutput spendableOutputDescriptorStaticOutput:
|
||||
// //ignore
|
||||
// break;
|
||||
// case SpendableOutputDescriptor.SpendableOutputDescriptor_StaticPaymentOutput spendableOutputDescriptorStaticPaymentOutput:
|
||||
// spendableOutputDescriptorStaticPaymentOutput.static_payment_output.psb
|
||||
// break;
|
||||
// default:
|
||||
// throw new ArgumentOutOfRangeException(nameof(Descriptor));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
public class CoinWithKey : Coin,ISignableCoin
|
||||
{
|
||||
public Key Key { get; }
|
||||
|
||||
public CoinWithKey(OutPoint fromOutpoint, TxOut fromTxOut, Key key) : base(fromOutpoint, fromTxOut)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public async Task<PSBT> Sign(PSBT psbt)
|
||||
{
|
||||
return psbt.SignWithKeys(Key);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISignableCoin : ICoin
|
||||
{
|
||||
Task<PSBT> Sign(PSBT psbt);
|
||||
}
|
||||
|
||||
public async Task<TxResp[]> GetTransactions()
|
||||
{
|
||||
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
|
||||
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
|
||||
var res= await _btcPayConnectionManager.HubProxy.GetTransactions(identifiersWhichWeCanDeriveKeysFor).RunSync();
|
||||
return res.SelectMany(pair => pair.Value).OrderByDescending(resp => resp.Timestamp).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<ICoin>> GetUTXOS()
|
||||
{
|
||||
var identifiers = WalletConfig.Derivations.Values.Select(derivation => derivation.Identifier).ToArray();
|
||||
var utxos = await _btcPayConnectionManager.HubProxy.GetUTXOs(identifiers).RunSync();
|
||||
var identifiersWhichWeCanDeriveKeysFor = WalletConfig.Derivations.Values
|
||||
.Where(derivation => derivation.Descriptor is not null).Select(derivation => derivation.Identifier).ToArray();
|
||||
var result = new List<ICoin>();
|
||||
|
||||
var utxosThatWeCanDeriveKeysFor = utxos.Where(utxo => identifiersWhichWeCanDeriveKeysFor.Contains(utxo.Identifier)).ToArray();
|
||||
foreach (var coin in utxosThatWeCanDeriveKeysFor)
|
||||
{
|
||||
var derivation =
|
||||
WalletConfig.Derivations.Values.First(derivation => derivation.Identifier == coin.Identifier);
|
||||
var data = derivation.Descriptor.ExtractFromDescriptor(Network);
|
||||
if (data is null)
|
||||
continue;
|
||||
var coinKeyPath = KeyPath.Parse(coin.Path);
|
||||
var key = new Mnemonic(WalletConfig.Mnemonic).DeriveExtKey().Derive(data.Value.Item2.KeyPath)
|
||||
.Derive(coinKeyPath).PrivateKey;
|
||||
var c = ToCoin(coin);
|
||||
|
||||
|
||||
result.Add(new CoinWithKey(c.Outpoint, c.TxOut, key));
|
||||
|
||||
}
|
||||
// if (WalletConfig.Derivations.TryGetValue(WalletDerivation.SpendableOutputs, out var spendableOutputDerivation))
|
||||
// {
|
||||
//
|
||||
// var spendableOutputUtxos = utxos.Where(response => response.Identifier == spendableOutputDerivation.Identifier).ToArray();
|
||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
// var scipts = spendableOutputUtxos.Select(response => response.Script).Distinct();
|
||||
// var spendableCoins = await context.SpendableCoins.Where(coin => scipts.Contains(coin.Script)).ToListAsync();
|
||||
//
|
||||
// foreach (var spendableOutputUtxo in spendableOutputUtxos)
|
||||
// {
|
||||
// var spendableCoin = spendableCoins.FirstOrDefault(coin => coin.Script == spendableOutputUtxo.Script);
|
||||
// if (spendableCoin is null)
|
||||
// continue;
|
||||
// var coin = ToCoin(spendableOutputUtxo);
|
||||
// var data = SpendableOutputDescriptor.read(spendableCoin.Data);
|
||||
// if(data is Result_SpendableOutputDescriptorDecodeErrorZ.Result_SpendableOutputDescriptorDecodeErrorZ_OK ok)
|
||||
// result.Add(new SpendableOutputDescriptorCoin(coin.Outpoint, coin.TxOut, ok.res));
|
||||
// }
|
||||
// }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async Task<(NBitcoin.Transaction Tx, ICoin[] SpentCoins, NBitcoin.Script Change)> CreateTransaction(
|
||||
List<TxOut> txOuts, FeeRate? feeRate, List<Coin> explicitIns = null)
|
||||
{
|
||||
var availableCoins = (await GetUTXOS()).ToList();
|
||||
feeRate ??= await GetFeeRate(1);
|
||||
//TODO: do not hardcode this constant
|
||||
var changeScript = await DeriveScript(WalletDerivation.NativeSegwit);
|
||||
var txBuilder = Network
|
||||
.CreateTransactionBuilder()
|
||||
.SetChange(changeScript)
|
||||
.SendEstimatedFees(feeRate);
|
||||
|
||||
txBuilder = txOuts.Aggregate(txBuilder, (current, c) => current.Send(c.ScriptPubKey, c.Value));
|
||||
txBuilder.SendAllRemainingToChange();
|
||||
|
||||
NBitcoin.Transaction? tx;
|
||||
if (explicitIns?.Any() is true)
|
||||
{
|
||||
txBuilder.AddCoins(explicitIns.ToArray());
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
tx = txBuilder.BuildTransaction(true);
|
||||
return (tx, txBuilder.FindSpentCoins(tx), changeScript);
|
||||
}
|
||||
catch (NotEnoughFundsException e)
|
||||
{
|
||||
if (!availableCoins.Any())
|
||||
throw;
|
||||
var newCoin = availableCoins.First();
|
||||
//TODO: switch to nuilding a psbt and signing with the ISignableCoin interface
|
||||
if(newCoin is CoinWithKey newCoinWithKey)
|
||||
{
|
||||
txBuilder.AddCoins(newCoin);
|
||||
txBuilder.AddKeys(newCoinWithKey.Key);
|
||||
}
|
||||
availableCoins.Remove(newCoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveDerivation(params string[] key)
|
||||
{
|
||||
await _controlSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (State != OnChainWalletState.Loaded || WalletConfig is null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot remove deriv in current state");
|
||||
}
|
||||
|
||||
var updated = key.Aggregate(false, (current, k) => current || WalletConfig.Derivations.Remove(k));
|
||||
if (updated)
|
||||
await _configProvider.Set(WalletConfig.Key, WalletConfig);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
public async Task<BestBlockResponse?> GetBestBlock()
|
||||
{
|
||||
var res = await _memoryCache.GetOrCreateAsync("bestblock", async entry =>
|
||||
{
|
||||
_logger.LogInformation("Getting best block");
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
try
|
||||
{
|
||||
|
||||
return await _btcPayConnectionManager.HubProxy.GetBestBlock().RunSync();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error getting best block");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("Got best block");
|
||||
}
|
||||
});
|
||||
if (res is null)
|
||||
{
|
||||
_memoryCache.Remove("bestblock");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public async Task BroadcastTransaction(Transaction valueTx, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _btcPayConnectionManager.HubProxy.BroadcastTransaction(valueTx.ToHex()).RunSync();
|
||||
}
|
||||
|
||||
public async Task<FeeRate> GetFeeRate(int blockTarget)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync($"feerate_{blockTarget}", async entry =>
|
||||
{
|
||||
_logger.LogInformation("Getting fee rate for block target {BlockTarget}", blockTarget);
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||
try
|
||||
{
|
||||
return new FeeRate(await _btcPayConnectionManager.HubProxy.GetFeeRate(blockTarget).RunSync());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("Got fee rate for block target {BlockTarget}", blockTarget);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error getting fee rate, using hardcoded 100");
|
||||
return new FeeRate(100m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OnChainWalletState
|
||||
{
|
||||
Init,
|
||||
NotConfigured,
|
||||
WaitingForConnection,
|
||||
Loading,
|
||||
Loaded
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
Pending,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
14
BTCPayApp.Core/Auth/AppPolicies.cs
Normal file
14
BTCPayApp.Core/Auth/AppPolicies.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AppPolicies
|
||||
{
|
||||
public const string CanModifySettings = "btcpay.plugin.app.canmodifysettings";
|
||||
|
||||
public static IEnumerable<string> AllPolicies
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CanModifySettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@ -13,67 +11,67 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, IHostedService
|
||||
public class AuthStateProvider(
|
||||
IHttpClientFactory clientFactory,
|
||||
IAuthorizationService authService,
|
||||
ISecureConfigProvider secureProvider,
|
||||
ConfigProvider configProvider,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions)
|
||||
: AuthenticationStateProvider, IAccountManager, IHostedService
|
||||
{
|
||||
private const string AccountKeyPrefix = "Account";
|
||||
private const string CurrentAccountKey = "CurrentAccount";
|
||||
private bool _isInitialized;
|
||||
private BTCPayAccount? _account;
|
||||
|
||||
// TODO: Move _userInfo to state
|
||||
private AppUserInfo? _userInfo;
|
||||
private bool _refreshUserInfo;
|
||||
private string? _currentStoreId;
|
||||
private CancellationTokenSource? _pingCts;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private readonly ClaimsPrincipal _unauthenticated = new(new ClaimsIdentity());
|
||||
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
|
||||
private readonly IAuthorizationService _authService;
|
||||
private readonly IConfigProvider _config;
|
||||
|
||||
public BTCPayAccount? GetAccount() => _account;
|
||||
public AppUserInfo? GetUserInfo() => _userInfo;
|
||||
public BTCPayAccount? Account { get; private set; }
|
||||
public AppUserInfo? UserInfo { get; private set; }
|
||||
public AppUserStoreInfo? CurrentStore => string.IsNullOrEmpty(_currentStoreId) ? null : GetUserStore(_currentStoreId);
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnStoreChanged { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChanged { get; set; }
|
||||
public AsyncEventHandler<string>? OnEncryptionKeyChanged { get; set; }
|
||||
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
|
||||
public AuthStateProvider(
|
||||
IConfigProvider config,
|
||||
IAuthorizationService authService,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_config = config;
|
||||
_authService = authService;
|
||||
_identityOptions = identityOptions;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = PingOccasionally();
|
||||
}
|
||||
|
||||
private async Task PingOccasionally()
|
||||
{
|
||||
while (_userInfo != null)
|
||||
{
|
||||
await GetAuthenticationStateAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
_pingCts = new CancellationTokenSource();
|
||||
_ = PingOccasionally(_pingCts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_pingCts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public BTCPayAppClient GetClient(string? baseUri = null)
|
||||
private async Task PingOccasionally(CancellationToken pingCtsToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(_account?.BaseUri))
|
||||
while (pingCtsToken.IsCancellationRequested is false)
|
||||
{
|
||||
await GetAuthenticationStateAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), pingCtsToken);
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(Account?.BaseUri))
|
||||
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
|
||||
var client = new BTCPayAppClient(baseUri ?? _account!.BaseUri);
|
||||
if (string.IsNullOrEmpty(baseUri) && !string.IsNullOrEmpty(_account?.AccessToken) && !string.IsNullOrEmpty(_account.RefreshToken))
|
||||
client.SetAccess(_account.AccessToken, _account.RefreshToken, _account.AccessExpiry.GetValueOrDefault());
|
||||
client.AccessRefreshed += OnAccessRefresh;
|
||||
return client;
|
||||
token ??= Account?.ModeToken ?? Account?.OwnerToken;
|
||||
return new BTCPayAppClient(baseUri ?? Account!.BaseUri, token, clientFactory.CreateClient());
|
||||
}
|
||||
|
||||
public async Task<string?> GetEncryptionKey()
|
||||
{
|
||||
return await secureProvider.Get<string>("encryptionKey");
|
||||
}
|
||||
|
||||
public async Task SetEncryptionKey(string value)
|
||||
{
|
||||
await secureProvider.Set("encryptionKey", value);
|
||||
OnEncryptionKeyChanged?.Invoke(this, value);
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
@ -85,49 +83,56 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
await _semaphore.WaitAsync();
|
||||
|
||||
// initialize with persisted account
|
||||
if (!_isInitialized && _account == null)
|
||||
if (!_isInitialized && Account == null)
|
||||
{
|
||||
_account = await GetCurrentAccount();
|
||||
Account = await secureProvider.Get<BTCPayAccount>(BTCPayAccount.Key);
|
||||
_currentStoreId = (await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key))?.CurrentStoreId;
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
var oldUserInfo = _userInfo;
|
||||
if (_userInfo == null && _account?.HasTokens is true)
|
||||
var oldUserInfo = UserInfo;
|
||||
var hasOwnerToken = !string.IsNullOrEmpty(Account?.OwnerToken);
|
||||
var hasModeToken = !string.IsNullOrEmpty(Account?.ModeToken);
|
||||
var needsRefresh = _refreshUserInfo || UserInfo == null;
|
||||
if (needsRefresh && hasOwnerToken)
|
||||
{
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
await FetchUserInfo(cts.Token);
|
||||
UserInfo = await GetClient().GetUserInfo(cts.Token);
|
||||
_refreshUserInfo = false;
|
||||
}
|
||||
|
||||
if (_userInfo != null)
|
||||
if (Account != null && UserInfo != null)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, _userInfo.UserId!),
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.UserNameClaimType, _userInfo.Name ?? _userInfo.Email!),
|
||||
new(_identityOptions.CurrentValue.ClaimsIdentity.EmailClaimType, _userInfo.Email!)
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, UserInfo.UserId!),
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.UserNameClaimType, UserInfo.Name ?? UserInfo.Email!),
|
||||
new(identityOptions.CurrentValue.ClaimsIdentity.EmailClaimType, UserInfo.Email!)
|
||||
};
|
||||
if (_userInfo.Roles?.Any() is true)
|
||||
claims.AddRange(_userInfo.Roles.Select(role =>
|
||||
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
|
||||
if (_userInfo.Stores?.Any() is true)
|
||||
claims.AddRange(_userInfo.Stores.Select(store =>
|
||||
new Claim(store.Id, string.Join(',', store.Permissions))));
|
||||
user = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationSchemes.GreenfieldBearer));
|
||||
|
||||
// update account user info
|
||||
_account!.SetInfo(_userInfo.Email!, _userInfo.Name, _userInfo.ImageUrl);
|
||||
await UpdateAccount(_account);
|
||||
if (UserInfo.Roles?.Any() is true)
|
||||
claims.AddRange(UserInfo.Roles.Select(role =>
|
||||
new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, role)));
|
||||
if (UserInfo.Stores?.Any() is true)
|
||||
claims.AddRange(UserInfo.Stores.Select(store =>
|
||||
new Claim(store.Id, string.Join(',', store.Permissions ?? []))));
|
||||
if (hasOwnerToken && !hasModeToken)
|
||||
claims.Add(new Claim(identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, "DeviceOwner"));
|
||||
user = new ClaimsPrincipal(new ClaimsIdentity(claims, "Greenfield"));
|
||||
}
|
||||
|
||||
var res = new AuthenticationState(user);
|
||||
if (AppUserInfo.Equals(oldUserInfo, _userInfo))
|
||||
return res;
|
||||
if (AppUserInfo.Equals(oldUserInfo, UserInfo)) return res;
|
||||
|
||||
OnUserInfoChanged?.Invoke(this, UserInfo);
|
||||
if (Account != null && UserInfo != null)
|
||||
await UpdateAccount(Account);
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(res));
|
||||
return res;
|
||||
}
|
||||
catch
|
||||
{
|
||||
UserInfo = null;
|
||||
return new AuthenticationState(user);
|
||||
}
|
||||
finally
|
||||
@ -138,78 +143,78 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
|
||||
public async Task<bool> CheckAuthenticated(bool refreshUser = false)
|
||||
{
|
||||
if (refreshUser) await FetchUserInfo();
|
||||
if (refreshUser) _refreshUserInfo = true;
|
||||
await GetAuthenticationStateAsync();
|
||||
return _userInfo != null;
|
||||
return UserInfo != null;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorized(string policy, object? resource = null)
|
||||
{
|
||||
var authState = await GetAuthenticationStateAsync();
|
||||
var result = await _authService.AuthorizeAsync(authState.User, resource, policy);
|
||||
var result = await authService.AuthorizeAsync(authState.User, resource, policy);
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
public async Task<FormResult> SetCurrentStoreId(string? storeId)
|
||||
{
|
||||
_userInfo = null;
|
||||
_account!.ClearAccess();
|
||||
await UpdateAccount(_account);
|
||||
await SetCurrentAccount(null);
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
var store = GetUserStore(storeId);
|
||||
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
|
||||
|
||||
if (store.Id != CurrentStore?.Id)
|
||||
await SetCurrentStore(store);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetCurrentStore(null);
|
||||
}
|
||||
return new FormResult(true);
|
||||
}
|
||||
|
||||
public async Task<FormResult> SetCurrentStoreId(string storeId)
|
||||
private async Task SetCurrentStore(AppUserStoreInfo? store)
|
||||
{
|
||||
var store = GetUserStore(storeId);
|
||||
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
|
||||
if (_currentStoreId == store?.Id) return;
|
||||
|
||||
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
|
||||
string? message = null;
|
||||
if (store != null)
|
||||
store = await EnsureStorePos(store);
|
||||
|
||||
// create associated POS app if there is none
|
||||
if (string.IsNullOrEmpty(store.PosAppId))
|
||||
_currentStoreId = store?.Id;
|
||||
|
||||
var appConfig = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key) ?? new BTCPayAppConfig();
|
||||
appConfig.CurrentStoreId = _currentStoreId;
|
||||
await configProvider.Set(BTCPayAppConfig.Key, appConfig, true);
|
||||
|
||||
OnStoreChanged?.Invoke(this, store);
|
||||
}
|
||||
|
||||
public async Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(store.PosAppId) || forceCreate is true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var posConfig = new PointOfSaleAppRequest { AppName = store.Name, DefaultView = PosViewType.Light };
|
||||
var app = await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
|
||||
message = $"The Point of Sale called \"{app.AppName}\" has been created for use with the app.";
|
||||
|
||||
await FetchUserInfo();
|
||||
await GetClient().CreatePointOfSaleApp(store.Id, posConfig);
|
||||
await CheckAuthenticated(true);
|
||||
store = GetUserStore(store.Id)!;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new FormResult(false, e.Message);
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
_account!.CurrentStoreId = storeId;
|
||||
await UpdateAccount(_account);
|
||||
OnAfterStoreChange?.Invoke(this, store);
|
||||
|
||||
return new FormResult(true, string.IsNullOrEmpty(message) ? null : [message]);
|
||||
return store;
|
||||
}
|
||||
|
||||
public async Task UnsetCurrentStore()
|
||||
private AppUserStoreInfo? GetUserStore(string storeId)
|
||||
{
|
||||
_account!.CurrentStoreId = null;
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
|
||||
public AppUserStoreInfo? GetUserStore(string storeId)
|
||||
{
|
||||
return _userInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
|
||||
}
|
||||
|
||||
public AppUserStoreInfo? GetCurrentStore()
|
||||
{
|
||||
var storeId = _account?.CurrentStoreId;
|
||||
return string.IsNullOrEmpty(storeId) ? null : GetUserStore(storeId);
|
||||
return UserInfo?.Stores?.FirstOrDefault(store => store.Id == storeId);
|
||||
}
|
||||
|
||||
public async Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default)
|
||||
{
|
||||
var urlParts = inviteUrl.Split("/invite/");
|
||||
var urlParts = inviteUrl.Split(Constants.InviteSeparator);
|
||||
var serverUrl = urlParts.First();
|
||||
var pathParts = urlParts.Last().Split("/");
|
||||
var payload = new AcceptInviteRequest
|
||||
@ -220,8 +225,8 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).AcceptInvite(payload, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, response.Email);
|
||||
await SetCurrentAccount(account);
|
||||
var account = new BTCPayAccount(serverUrl, response.Email!);
|
||||
await SetAccount(account);
|
||||
var message = "Invitation accepted.";
|
||||
if (response.EmailHasBeenConfirmed is true)
|
||||
message += " Your email has been confirmed.";
|
||||
@ -232,9 +237,22 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
: " Please set your password.";
|
||||
return new FormResult<AcceptInviteResult>(true, message, response);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new FormResult<AcceptInviteResult>(false, "Invalid invitation.", null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetClient(serverUrl).LoginInfo(email, cancellation.GetValueOrDefault());
|
||||
return new FormResult<LoginInfoResult>(true, string.Empty, response);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<AcceptInviteResult>(false, e.Message, null);
|
||||
return new FormResult<LoginInfoResult>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,11 +266,10 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).Login(payload, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -261,16 +278,36 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var client = GetClient(serverUrl);
|
||||
var response = await client.Login(code, cancellation.GetValueOrDefault());
|
||||
var account = await GetAccount(serverUrl, email);
|
||||
account.SetAccess(response.AccessToken, response.RefreshToken, response.ExpiresIn, expiryOffset);
|
||||
await SetCurrentAccount(account);
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
var clientWithToken = GetClient(serverUrl, response.AccessToken);
|
||||
var userInfo = await clientWithToken.GetUserInfo();
|
||||
email = userInfo?.Email!;
|
||||
}
|
||||
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult(false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult> AddAccountWithEncyptionKey(string serverUrl, string email, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var account = new BTCPayAccount(serverUrl, email);
|
||||
await SetAccount(account);
|
||||
await SetEncryptionKey(key);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -281,31 +318,31 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
|
||||
public async Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default)
|
||||
{
|
||||
var payload = new SignupRequest
|
||||
var payload = new CreateApplicationUserRequest
|
||||
{
|
||||
Email = email,
|
||||
Password = password
|
||||
};
|
||||
try
|
||||
{
|
||||
var expiryOffset = DateTimeOffset.Now;
|
||||
var response = await GetClient(serverUrl).RegisterUser(payload, cancellation.GetValueOrDefault());
|
||||
var account = new BTCPayAccount(serverUrl, email);
|
||||
var message = "Account created.";
|
||||
if (response.ContainsKey("accessToken"))
|
||||
{
|
||||
var access = response.ToObject<AccessTokenResponse>();
|
||||
account.SetAccess(access.AccessToken, access.RefreshToken, access.ExpiresIn, expiryOffset);
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
if (string.IsNullOrEmpty(access?.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
account.OwnerToken = access.AccessToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
var signup = response.ToObject<SignupResult>();
|
||||
if (signup.RequiresConfirmedEmail)
|
||||
var signup = response.ToObject<ApplicationUserData>();
|
||||
if (signup?.RequiresEmailConfirmation is true)
|
||||
message += " Please confirm your email.";
|
||||
if (signup.RequiresUserApproval)
|
||||
if (signup?.RequiresApproval is true)
|
||||
message += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
await SetCurrentAccount(account);
|
||||
await SetAccount(account);
|
||||
return new FormResult(true, message);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -325,7 +362,14 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
try
|
||||
{
|
||||
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
|
||||
await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
|
||||
var response = await GetClient(serverUrl).ResetPassword(payload, cancellation.GetValueOrDefault());
|
||||
if (response?.ContainsKey("accessToken") is true)
|
||||
{
|
||||
var access = response.ToObject<AuthenticationResponse>();
|
||||
var account = new BTCPayAccount(serverUrl, email, access!.AccessToken);
|
||||
await SetAccount(account);
|
||||
}
|
||||
|
||||
return new FormResult(true, isForgotStep
|
||||
? "You should have received an email with a password reset code."
|
||||
: "Your password has been reset.");
|
||||
@ -354,32 +398,23 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default)
|
||||
public async Task<FormResult> SwitchMode(string storeId, string mode, CancellationToken? cancellation = default)
|
||||
{
|
||||
var payload = new UpdateApplicationUserRequest
|
||||
if (Account == null || !string.IsNullOrEmpty(Account.ModeToken))
|
||||
return new FormResult(false, "Cannot switch mode in current state.");
|
||||
|
||||
var payload = new SwitchModeRequest
|
||||
{
|
||||
Email = email,
|
||||
Name = name,
|
||||
ImageUrl = imageUrl
|
||||
StoreId = storeId,
|
||||
Mode = mode
|
||||
};
|
||||
try
|
||||
{
|
||||
var userData = await GetClient().UpdateCurrentUser(payload, cancellation.GetValueOrDefault());
|
||||
_account!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
_userInfo!.SetInfo(userData.Email!, userData.Name, userData.ImageUrl);
|
||||
return new FormResult<ApplicationUserData>(true, "Your account info has been changed.", userData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult<ApplicationUserData>(false, e.Message, null);
|
||||
}
|
||||
}
|
||||
var response = await GetClient().SwitchMode(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
|
||||
public async Task<FormResult> RefreshAccess(CancellationToken? cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient().RefreshAccess(_account!.RefreshToken, cancellation);
|
||||
Account.ModeToken = response.AccessToken;
|
||||
await SetAccount(Account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -388,66 +423,53 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnAccessRefresh(object? sender, AccessTokenResult access)
|
||||
public async Task<FormResult> SwitchToOwner(string password, string? otp = null, CancellationToken? cancellation = default)
|
||||
{
|
||||
if (_account == null) return;
|
||||
_account.SetAccess(access.AccessToken, access.RefreshToken, access.Expiry);
|
||||
await UpdateAccount(_account);
|
||||
}
|
||||
if (Account == null || string.IsNullOrEmpty(Account.ModeToken) || string.IsNullOrEmpty(Account.OwnerToken))
|
||||
return new FormResult(false, "Cannot switch user in current state.");
|
||||
|
||||
private static string GetKey(string accountId) => $"{AccountKeyPrefix}:{accountId}";
|
||||
|
||||
public async Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null)
|
||||
{
|
||||
var prefix = $"{AccountKeyPrefix}:" + (hostFilter == null ? "" : $"{hostFilter}:");
|
||||
var keys = (await _config.List(prefix)).ToArray();
|
||||
var accounts = new List<BTCPayAccount>();
|
||||
foreach (var key in keys)
|
||||
var payload = new LoginRequest
|
||||
{
|
||||
var account = await _config.Get<BTCPayAccount>(key);
|
||||
accounts.Add(account!);
|
||||
Email = Account.Email,
|
||||
Password = password,
|
||||
TwoFactorCode = otp
|
||||
};
|
||||
try
|
||||
{
|
||||
var response = await GetClient().Login(payload, cancellation.GetValueOrDefault());
|
||||
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
|
||||
Account.ModeToken = null;
|
||||
await SetAccount(Account);
|
||||
return new FormResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FormResult(false, e.Message);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public async Task UpdateAccount(BTCPayAccount account)
|
||||
public async Task Logout()
|
||||
{
|
||||
await _config.Set(GetKey(account.Id), account);
|
||||
if (Account == null) return;
|
||||
Account.OwnerToken = Account.ModeToken = null;
|
||||
await SetAccount(Account);
|
||||
}
|
||||
|
||||
public async Task RemoveAccount(BTCPayAccount account)
|
||||
private async Task UpdateAccount(BTCPayAccount account)
|
||||
{
|
||||
await _config.Set<BTCPayAccount>(GetKey(account.Id), null);
|
||||
await secureProvider.Set(BTCPayAccount.Key, account);
|
||||
}
|
||||
|
||||
private async Task<BTCPayAccount> GetAccount(string serverUrl, string email)
|
||||
private async Task SetAccount(BTCPayAccount account)
|
||||
{
|
||||
var accountId = BTCPayAccount.GetId(serverUrl, email);
|
||||
var account = await _config.Get<BTCPayAccount>(GetKey(accountId));
|
||||
return account ?? new BTCPayAccount(serverUrl, email);
|
||||
}
|
||||
var storeId = CurrentStore?.Id;
|
||||
|
||||
private async Task<BTCPayAccount?> GetCurrentAccount()
|
||||
{
|
||||
var accountId = await _config.Get<string>(CurrentAccountKey);
|
||||
if (string.IsNullOrEmpty(accountId)) return null;
|
||||
return await _config.Get<BTCPayAccount>(GetKey(accountId));
|
||||
}
|
||||
|
||||
private async Task SetCurrentAccount(BTCPayAccount? account)
|
||||
{
|
||||
OnBeforeAccountChange?.Invoke(this, _account);
|
||||
if (account != null) await UpdateAccount(account);
|
||||
await _config.Set(CurrentAccountKey, account?.Id);
|
||||
_account = account;
|
||||
_userInfo = null;
|
||||
await UpdateAccount(account);
|
||||
Account = account;
|
||||
UserInfo = null;
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
OnAfterAccountChange?.Invoke(this, _account);
|
||||
}
|
||||
|
||||
private async Task FetchUserInfo(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_userInfo = await GetClient().GetUserInfo(cancellationToken);
|
||||
if (!string.IsNullOrEmpty(storeId)) await SetCurrentStoreId(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
76
BTCPayApp.Core/Auth/AuthorizationHandler.cs
Normal file
76
BTCPayApp.Core/Auth/AuthorizationHandler.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class AuthorizationHandler(IOptionsMonitor<IdentityOptions> identityOptions) : AuthorizationHandler<PolicyRequirement>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity?.AuthenticationType != "Greenfield")
|
||||
return Task.CompletedTask;
|
||||
|
||||
var userId = context.User.Claims.FirstOrDefault(c => c.Type == identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType)?.Value;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Task.CompletedTask;
|
||||
|
||||
var permissionSet = new PermissionSet();
|
||||
var success = false;
|
||||
var isAdmin = context.User.IsInRole("ServerAdmin");
|
||||
var isOwner = context.User.IsInRole("DeviceOwner");
|
||||
var storeId = context.Resource as string;
|
||||
var policy = requirement.Policy;
|
||||
var requiredUnscoped = false;
|
||||
if (policy.EndsWith(':'))
|
||||
{
|
||||
policy = policy[..^1];
|
||||
requiredUnscoped = true;
|
||||
storeId = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
var permissions = context.User.Claims.FirstOrDefault(c => c.Type == storeId)?.Value;
|
||||
if (!string.IsNullOrEmpty(permissions))
|
||||
{
|
||||
permissionSet = new PermissionSet(permissions.Split(',')
|
||||
.Select(s => Permission.TryParse(s, out var permission) ? permission : null)
|
||||
.Where(s => s != null).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
if (Policies.IsServerPolicy(policy) && isAdmin)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsUserPolicy(policy) && !string.IsNullOrEmpty(userId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsStorePolicy(policy) && !string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
if (!success && permissionSet.Contains(policy, storeId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
|
||||
if (!success && requiredUnscoped && string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
else if (Policies.IsPluginPolicy(policy) && policy.StartsWith("btcpay.plugin.app"))
|
||||
{
|
||||
success = isOwner;
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -1,34 +1,33 @@
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public interface IAccountManager
|
||||
{
|
||||
public BTCPayAccount? GetAccount();
|
||||
public Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null);
|
||||
public AppUserInfo? GetUserInfo();
|
||||
public BTCPayAppClient GetClient(string? baseUri = null);
|
||||
public BTCPayAccount? Account { get; }
|
||||
public AppUserInfo? UserInfo { get; }
|
||||
public AppUserStoreInfo? CurrentStore { get; }
|
||||
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null);
|
||||
public Task<string?> GetEncryptionKey();
|
||||
public Task SetEncryptionKey(string value);
|
||||
public Task<bool> CheckAuthenticated(bool refreshUser = false);
|
||||
public Task<bool> IsAuthorized(string policy, object? resource = null);
|
||||
public Task<FormResult> AddAccountWithEncyptionKey(string serverUrl, string email, string key);
|
||||
public Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Login(string serverUrl, string email, string password, string? otp, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> ResetPassword(string serverUrl, string email, string? resetCode, string? newPassword, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<ApplicationUserData>> ChangePassword(string currentPassword, string newPassword, CancellationToken? cancellation = default);
|
||||
public Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> RefreshAccess(CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SetCurrentStoreId(string storeId);
|
||||
public Task UnsetCurrentStore();
|
||||
public AppUserStoreInfo? GetCurrentStore();
|
||||
public AppUserStoreInfo? GetUserStore(string storeId);
|
||||
public Task<FormResult> SwitchMode(string storeId, string mode, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SwitchToOwner(string password, string? otp, CancellationToken? cancellation = default);
|
||||
public Task<FormResult> SetCurrentStoreId(string? storeId);
|
||||
public Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false);
|
||||
public Task Logout();
|
||||
public Task UpdateAccount(BTCPayAccount account);
|
||||
public Task RemoveAccount(BTCPayAccount account);
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChanged { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnStoreChanged { get; set; }
|
||||
public AsyncEventHandler<string>? OnEncryptionKeyChanged { get; set; }
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class Invoice
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? OrderId { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
namespace BTCPayApp.Core.Auth;
|
||||
|
||||
public class Notification
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string? Body { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
}
|
||||
@ -1,48 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAccount(string baseUri, string email)
|
||||
public class BTCPayAccount(string baseUri, string email, string? ownerToken = null)
|
||||
{
|
||||
public static string GetId(string baseUri, string email) => $"{new Uri(baseUri).Host}:{email}";
|
||||
public readonly string Id = GetId(baseUri, email);
|
||||
public const string Key = "account";
|
||||
public string Id { get; private set; } = $"{new Uri(baseUri).Host}:{email}";
|
||||
public string BaseUri { get; private set; } = WithTrailingSlash(baseUri);
|
||||
public string Email { get; private set; } = email;
|
||||
public string? Name { get; set; }
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTimeOffset? AccessExpiry { get; set; }
|
||||
public string? CurrentStoreId { get; set; }
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, long expiresInSeconds, DateTimeOffset? expiryOffset = null)
|
||||
{
|
||||
var expiry = (expiryOffset ?? DateTimeOffset.Now) + TimeSpan.FromSeconds(expiresInSeconds);
|
||||
SetAccess(accessToken, refreshToken, expiry);
|
||||
}
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, DateTimeOffset expiry)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
AccessExpiry = expiry;
|
||||
}
|
||||
|
||||
public void ClearAccess()
|
||||
{
|
||||
AccessToken = RefreshToken = null;
|
||||
AccessExpiry = null;
|
||||
}
|
||||
|
||||
public void SetInfo(string email, string? name, string? imageUrl)
|
||||
{
|
||||
Email = email;
|
||||
Name = name;
|
||||
ImageUrl = imageUrl;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasTokens => !string.IsNullOrEmpty(AccessToken) && !string.IsNullOrEmpty(RefreshToken);
|
||||
public string? OwnerToken { get; set; } = ownerToken;
|
||||
public string? ModeToken { get; set; }
|
||||
|
||||
private static string WithTrailingSlash(string s)
|
||||
{
|
||||
|
||||
@ -1,51 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.6" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.6" />
|
||||
<PackageReference Include="org.ldk" Version="0.0.123" />
|
||||
<PackageReference Include="TypedSignalR.Client" Version="3.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj"/>
|
||||
<PackageReference Include="AsyncKeyedLock" Version="7.1.4"/>
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.1.0"/>
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.SqlLite" Version="8.1.2"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="NBitcoin" Version="8.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageReference Include="org.ldk" Version="0.1.2" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SQLite.Maui" Version="1.9.7" />
|
||||
<PackageReference Include="TypedSignalR.Client" Version="3.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.15" />
|
||||
<PackageReference Include="VSS" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,129 +1,22 @@
|
||||
using System.Net;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Web;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using AccessTokenResponse = BTCPayApp.Core.AspNetRip.AccessTokenResponse;
|
||||
using ProblemDetails = BTCPayApp.Core.AspNetRip.ProblemDetails;
|
||||
using RefreshRequest = BTCPayApp.Core.AspNetRip.RefreshRequest;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAppClient(string baseUri) : BTCPayServerClient(new Uri(baseUri))
|
||||
public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient? client = null) : BTCPayServerClient(new Uri(baseUri), apiKey, client)
|
||||
{
|
||||
private const string RefreshPath = "btcpayapp/refresh";
|
||||
private DateTimeOffset? AccessExpiry { get; set; } // TODO: Incorporate in refresh check
|
||||
private string? AccessToken { get; set; }
|
||||
private string? RefreshToken { get; set; }
|
||||
|
||||
public event EventHandler<AccessTokenResult>? AccessRefreshed;
|
||||
|
||||
public void SetAccess(string accessToken, string refreshToken, DateTimeOffset expiry)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
AccessExpiry = expiry;
|
||||
}
|
||||
|
||||
private void ClearAccess()
|
||||
{
|
||||
AccessToken = RefreshToken = null;
|
||||
AccessExpiry = null;
|
||||
}
|
||||
|
||||
protected override HttpRequestMessage CreateHttpRequest(string path, Dictionary<string, object>? queryPayload = null, HttpMethod? method = null)
|
||||
{
|
||||
var req = base.CreateHttpRequest(path, queryPayload, method);
|
||||
req.Headers.Add("User-Agent", "BTCPayAppClient");
|
||||
if (!string.IsNullOrEmpty(AccessToken))
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
|
||||
req.Headers.Add("Accept", "application/json");
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override async Task<T> HandleResponse<T>(HttpResponseMessage res)
|
||||
{
|
||||
if (res is { IsSuccessStatusCode: false })
|
||||
{
|
||||
var req = res.RequestMessage;
|
||||
if (res.StatusCode == HttpStatusCode.Unauthorized && !string.IsNullOrEmpty(RefreshToken))
|
||||
{
|
||||
// try refresh and recurse if the token could be renewed
|
||||
var uri = req!.RequestUri;
|
||||
var path = uri!.AbsolutePath;
|
||||
if (!path.EndsWith(RefreshPath))
|
||||
{
|
||||
var (refresh, _) = await RefreshAccess(RefreshToken);
|
||||
if (refresh != null)
|
||||
{
|
||||
if (req.Content is not null)
|
||||
{
|
||||
var content = await req.Content.ReadAsStringAsync();
|
||||
var payload = JsonConvert.DeserializeObject<T>(content);
|
||||
return await SendHttpRequest<T>(path, bodyPayload: payload, method: req.Method);
|
||||
}
|
||||
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
var queryPayload = query.HasKeys() ? query.AllKeys.ToDictionary(k => k, k => query[k]) : null;
|
||||
return await SendHttpRequest<T>(path, queryPayload, method: req.Method);
|
||||
}
|
||||
}
|
||||
ClearAccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
// try parsing as ProblemDetails
|
||||
try
|
||||
{
|
||||
var content = await res.Content.ReadAsStringAsync();
|
||||
var err = JsonConvert.DeserializeObject<ProblemDetails>(content);
|
||||
if (err?.Status != null && !string.IsNullOrEmpty(err.Detail))
|
||||
{
|
||||
var error = new GreenfieldAPIError("unauthorized", err.Detail);
|
||||
throw new GreenfieldAPIException(err.Status.Value, error);
|
||||
}
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
return await base.HandleResponse<T>(res);
|
||||
}
|
||||
|
||||
private AccessTokenResult HandleAccessTokenResponse(AccessTokenResponse response, DateTimeOffset expiryOffset)
|
||||
{
|
||||
var expiry = expiryOffset + TimeSpan.FromSeconds(response.ExpiresIn);
|
||||
SetAccess(response.AccessToken, response.RefreshToken, expiry);
|
||||
return new AccessTokenResult(response.AccessToken, response.RefreshToken, expiry);
|
||||
}
|
||||
|
||||
public async Task<(AccessTokenResult? success, string? errorCode)> RefreshAccess(string? refreshToken = null, CancellationToken? cancellation = default)
|
||||
{
|
||||
var token = refreshToken ?? RefreshToken;
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new ArgumentException("No refresh token present or provided.", nameof(refreshToken));
|
||||
|
||||
var payload = new RefreshRequest { RefreshToken = token };
|
||||
var now = DateTimeOffset.Now;
|
||||
try
|
||||
{
|
||||
var tokenResponse = await SendHttpRequest<AccessTokenResponse>(RefreshPath, bodyPayload: payload, method: HttpMethod.Post);
|
||||
var res = HandleAccessTokenResponse(tokenResponse, now);
|
||||
AccessRefreshed?.Invoke(this, res);
|
||||
return (res, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return (null, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AppInstanceInfo?> GetInstanceInfo(CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AppInstanceInfo>("btcpayapp/instance", null, HttpMethod.Get, cancellation);
|
||||
@ -139,30 +32,66 @@ public class BTCPayAppClient(string baseUri) : BTCPayServerClient(new Uri(baseUr
|
||||
return await SendHttpRequest<CreateStoreData>("btcpayapp/create-store", null, HttpMethod.Get, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject> RegisterUser(SignupRequest payload, CancellationToken cancellation)
|
||||
public async Task<JObject> RegisterUser(CreateApplicationUserRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<JObject>("btcpayapp/register", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AccessTokenResponse> Login(LoginRequest payload, CancellationToken cancellation)
|
||||
public async Task<LoginInfoResult> LoginInfo(string email, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
var payload = new Dictionary<string, object> { { "email", email } };
|
||||
return await SendHttpRequest<LoginInfoResult>("btcpayapp/login-info", payload, HttpMethod.Get, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AccessTokenResponse> Login(string loginCode, CancellationToken cancellation)
|
||||
public async Task<AuthenticationResponse> Login(LoginRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AccessTokenResponse>("btcpayapp/login/code", loginCode, HttpMethod.Post, cancellation);
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AcceptInviteResult> AcceptInvite(AcceptInviteRequest payload, CancellationToken cancellation)
|
||||
public async Task<AuthenticationResponse> SwitchMode(SwitchModeRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/switch-mode", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResponse> Login(string loginCode, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AuthenticationResponse>("btcpayapp/login/code", loginCode, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<AcceptInviteResult> AcceptInvite(AcceptInviteRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<AcceptInviteResult>("btcpayapp/accept-invite", payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation)
|
||||
public async Task<JObject?> ResetPassword(ResetPasswordRequest payload, CancellationToken cancellation = default)
|
||||
{
|
||||
var isForgotStep = string.IsNullOrEmpty(payload.ResetCode) && string.IsNullOrEmpty(payload.NewPassword);
|
||||
var path = isForgotStep ? "btcpayapp/forgot-password" : "btcpayapp/reset-password";
|
||||
await SendHttpRequest<EmptyResult>(path, payload, HttpMethod.Post, cancellation);
|
||||
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
if (req.DiscountPercent != null) query.Add("discount", req.DiscountPercent.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.Tip != null) query.Add("tip", req.Tip.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.PosData != null) query.Add("posData", req.PosData);
|
||||
return await SendHttpRequest<JObject?>($"apps/{req.AppId}/pos/light", query, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<string> SubmitLNURLWithdrawForInvoice(SubmitLnUrlRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<string>($"plugins/NFC", req, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public virtual async Task<T> UploadFileRequest<T>(string apiPath, StreamContent fileContent, string fileName, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
using MultipartFormDataContent multipartContent = new();
|
||||
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType);
|
||||
multipartContent.Add(fileContent, "file", fileName);
|
||||
var req = CreateHttpRequest(apiPath, null, HttpMethod.Post);
|
||||
req.Content = multipartContent;
|
||||
using var resp = await _httpClient.SendAsync(req, token);
|
||||
return await HandleResponse<T>(resp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ namespace BTCPayApp.Core;
|
||||
|
||||
public class BTCPayAppConfig
|
||||
{
|
||||
public const string Key = "AppConfig";
|
||||
public const string Key = "appconfig";
|
||||
public bool RecoveryPhraseVerified { get; set; }
|
||||
public bool UseBiometricAuth { get; set; }
|
||||
public string? Passcode { get; set; }
|
||||
public string? CurrentStoreId { get; set; }
|
||||
}
|
||||
|
||||
59
BTCPayApp.Core/BTCPayServer/AppToServerHelper.cs
Normal file
59
BTCPayApp.Core/BTCPayServer/AppToServerHelper.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public static class AppToServerHelper
|
||||
{
|
||||
|
||||
public static LightningInvoice ToInvoice(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningInvoice
|
||||
{
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = lightningPayment.Value,
|
||||
PaymentHash = lightningPayment.PaymentHash?.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
ExpiresAt = lightningPayment.AdditionalData[PaymentsManager.LightningPaymentExpiryKey].GetDateTimeOffset(),
|
||||
PaidAt = lightningPayment.Status == LightningPaymentStatus.Complete
|
||||
? DateTimeOffset.UtcNow
|
||||
: null, //TODO: store these in ln payment
|
||||
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
|
||||
Status = lightningPayment.Status == LightningPaymentStatus.Complete
|
||||
? LightningInvoiceStatus.Paid
|
||||
: lightningPayment.PaymentRequest?.ExpiryDate < DateTimeOffset.UtcNow
|
||||
? LightningInvoiceStatus.Expired
|
||||
: LightningInvoiceStatus.Unpaid,
|
||||
AmountReceived = lightningPayment.Status == LightningPaymentStatus.Complete? lightningPayment.Value: null
|
||||
};
|
||||
}
|
||||
|
||||
public static LightningPayment ToPayment(this AppLightningPayment lightningPayment)
|
||||
{
|
||||
return new LightningPayment
|
||||
{
|
||||
Id = lightningPayment.PaymentHash?.ToString(),
|
||||
Amount = LightMoney.MilliSatoshis(lightningPayment.Value),
|
||||
PaymentHash = lightningPayment.PaymentHash?.ToString(),
|
||||
Preimage = lightningPayment.Preimage,
|
||||
BOLT11 = lightningPayment.PaymentRequest?.ToString(),
|
||||
Status = lightningPayment.Status,
|
||||
Fee = lightningPayment.AdditionalData.TryGetValue("feePaid", out var feePaid) ? LightMoney.MilliSatoshis((long)feePaid.GetInt64()) : null,
|
||||
CreatedAt = lightningPayment.Timestamp
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<List<LightningPayment>> ToPayments(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToPayment).ToList();
|
||||
}
|
||||
|
||||
public static async Task<List<LightningInvoice>> ToInvoices(this Task<List<AppLightningPayment>> appLightningPayments)
|
||||
{
|
||||
var result = await appLightningPayments;
|
||||
return result.Select(ToInvoice).ToList();
|
||||
}
|
||||
}
|
||||
237
BTCPayApp.Core/BTCPayServer/BTCPayAppServerClient.cs
Normal file
237
BTCPayApp.Core/BTCPayServer/BTCPayAppServerClient.cs
Normal file
@ -0,0 +1,237 @@
|
||||
using System.Text;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using org.ldk.structs;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
|
||||
public class BTCPayAppServerClient(ILogger<BTCPayAppServerClient> _logger, IServiceProvider _serviceProvider)
|
||||
: IBTCPayAppHubClient
|
||||
{
|
||||
public event AsyncEventHandler<string>? OnNewBlock;
|
||||
public event AsyncEventHandler<TransactionDetectedRequest>? OnTransactionDetected;
|
||||
public event AsyncEventHandler<string>? OnNotifyNetwork;
|
||||
public event AsyncEventHandler<string>? OnServerNodeInfo;
|
||||
public event AsyncEventHandler<long?>? OnMasterUpdated;
|
||||
public event AsyncEventHandler<ServerEvent>? OnNotifyServerEvent;
|
||||
|
||||
private LDKNode? Node => _serviceProvider.GetRequiredService<LightningNodeManager>().Node;
|
||||
private PaymentsManager? PaymentsManager => Node?.PaymentsManager;
|
||||
private LightningAPIKeyManager? ApiKeyManager => Node?.ApiKeyManager;
|
||||
|
||||
public async Task NotifyServerEvent(ServerEvent ev)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerEvent: {Event}", ev.ToString());
|
||||
if (OnNotifyServerEvent is null) return;
|
||||
await OnNotifyServerEvent.Invoke(this, ev);
|
||||
}
|
||||
|
||||
public async Task NotifyNetwork(string network)
|
||||
{
|
||||
_logger.LogInformation("NotifyNetwork: {Network}", network);
|
||||
if (OnNotifyNetwork is null) return;
|
||||
await OnNotifyNetwork.Invoke(this, network);
|
||||
}
|
||||
|
||||
public async Task NotifyServerNode(string nodeInfo)
|
||||
{
|
||||
_logger.LogInformation("NotifyServerNode: {NodeInfo}", nodeInfo);
|
||||
if (OnServerNodeInfo is null) return;
|
||||
await OnServerNodeInfo.Invoke(this, nodeInfo);
|
||||
}
|
||||
|
||||
public async Task TransactionDetected(TransactionDetectedRequest request)
|
||||
{
|
||||
_logger.LogInformation("OnTransactionDetected: {TxId}", request.TxId);
|
||||
if (OnTransactionDetected is null) return;
|
||||
await OnTransactionDetected.Invoke(this, request);
|
||||
}
|
||||
|
||||
public async Task NewBlock(string block)
|
||||
{
|
||||
_logger.LogInformation("NewBlock: {Block}", block);
|
||||
if (OnNewBlock is null) return;
|
||||
await OnNewBlock.Invoke(this, block);
|
||||
}
|
||||
|
||||
public async Task StartListen(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
_serviceProvider
|
||||
.GetRequiredService<LightningNodeManager>().Node?
|
||||
.GetServiceProvider()
|
||||
.GetRequiredService<BTCPayPaymentsNotifier>()
|
||||
.StartListen();
|
||||
}
|
||||
|
||||
private async Task AssertPermission(string key, APIKeyPermission permission)
|
||||
{
|
||||
if (ApiKeyManager is null)
|
||||
throw new HubException("Api Key Manager not available");
|
||||
if (!await ApiKeyManager.CheckPermission(key, permission))
|
||||
throw new HubException("Permission denied");
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var descHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(createLightningInvoiceRequest.Description)),
|
||||
false);
|
||||
return (await PaymentsManager.RequestPayment(createLightningInvoiceRequest.Amount,
|
||||
createLightningInvoiceRequest.Expiry, descHash)).ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var invoices = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invoices.FirstOrDefault()?.ToInvoice();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var invoices = await PaymentsManager.List(payments =>
|
||||
payments.Where(payment => !payment.Inbound && payment.PaymentHash == paymentHash));
|
||||
return invoices.FirstOrDefault()?.ToPayment();
|
||||
}
|
||||
|
||||
public async Task CancelInvoice(string key, uint256 paymentHash)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Write);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
await PaymentsManager.CancelInbound(paymentHash);
|
||||
}
|
||||
|
||||
public async Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => !payment.Inbound))
|
||||
.ToPayments();
|
||||
}
|
||||
|
||||
public async Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
return await PaymentsManager.List(payments => payments.Where(payment => payment.Inbound)).ToInvoices();
|
||||
}
|
||||
|
||||
public async Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Write);
|
||||
if (PaymentsManager is null) throw new HubException("Payments Manager not available");
|
||||
|
||||
var config = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetConfig();
|
||||
var network = config?.NBitcoinNetwork;
|
||||
if (network is null) throw new HubException("Network info not available");
|
||||
|
||||
var bolt = BOLT11PaymentRequest.Parse(bolt11, network);
|
||||
try
|
||||
{
|
||||
var result = await PaymentsManager.PayInvoice(bolt,
|
||||
amountMilliSatoshi is null ? null : LightMoney.MilliSatoshis(amountMilliSatoshi.Value));
|
||||
return new PayResponse
|
||||
{
|
||||
Result = result.Status switch
|
||||
{
|
||||
LightningPaymentStatus.Unknown => PayResult.Unknown,
|
||||
LightningPaymentStatus.Pending => PayResult.Unknown,
|
||||
LightningPaymentStatus.Complete => PayResult.Ok,
|
||||
LightningPaymentStatus.Failed => PayResult.Error,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Details = new PayDetails
|
||||
{
|
||||
Preimage = result.Preimage is not null ? new uint256(result.Preimage) : null,
|
||||
Status = result.Status
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error paying invoice");
|
||||
return new PayResponse(PayResult.Error, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Task MasterUpdated(long? deviceIdentifier)
|
||||
{
|
||||
_logger.LogInformation("MasterUpdated: {DeviceIdentifier}", deviceIdentifier);
|
||||
OnMasterUpdated?.Invoke(this, deviceIdentifier);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetLightningNodeInfo(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (Node is null) throw new HubException("Lightning Node not available");
|
||||
|
||||
var config = await Node.GetConfig();
|
||||
var peers = await Node.GetPeers();
|
||||
var chans = await Node.GetChannels() ?? [];
|
||||
var channels = chans
|
||||
.Where(channel => channel.channelDetails is not null)
|
||||
.Select(channel => channel.channelDetails)
|
||||
.OfType<ChannelDetails>()
|
||||
.ToArray();
|
||||
var bb = await _serviceProvider.GetRequiredService<OnChainWalletManager>().GetBestBlock();
|
||||
return new LightningNodeInformation
|
||||
{
|
||||
Alias = config.Alias,
|
||||
Color = config.Color,
|
||||
Version = "preprepreprealpha",
|
||||
BlockHeight = bb?.BlockHeight ?? 0,
|
||||
PeersCount = peers.Length,
|
||||
ActiveChannelsCount = channels.Count(channel => channel.get_is_usable()),
|
||||
PendingChannelsCount = channels.Count(channel => !channel.get_is_usable() && !channel.get_is_channel_ready()),
|
||||
InactiveChannelsCount = channels.Count(channel => !channel.get_is_usable() && channel.get_is_channel_ready())
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LightningNodeBalance> GetLightningBalance(string key)
|
||||
{
|
||||
await AssertPermission(key, APIKeyPermission.Read);
|
||||
if (Node is null) throw new HubException("Lightning Node not available");
|
||||
|
||||
var chans = await Node.GetChannels() ?? [];
|
||||
var channels = chans
|
||||
.Where(channel => channel.channelDetails is not null)
|
||||
.Select(channel => channel.channelDetails)
|
||||
.OfType<ChannelDetails>()
|
||||
.ToArray();
|
||||
var balances = Node.ClaimableBalances;
|
||||
var closing = balances
|
||||
.Where(b => b is Balance.Balance_ClaimableAwaitingConfirmations)
|
||||
.ToArray();
|
||||
return new LightningNodeBalance
|
||||
{
|
||||
OffchainBalance = new OffchainBalance
|
||||
{
|
||||
Local = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_outbound_capacity_msat())),
|
||||
Remote = LightMoney.MilliSatoshis(channels.Sum(channel => channel.get_inbound_capacity_msat())),
|
||||
Closing = LightMoney.Satoshis(closing.Sum(balance => balance.claimable_amount_satoshis()))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
389
BTCPayApp.Core/BTCPayServer/BTCPayConnectionManager.cs
Normal file
389
BTCPayApp.Core/BTCPayServer/BTCPayConnectionManager.cs
Normal file
@ -0,0 +1,389 @@
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayConnectionManager(
|
||||
IServiceProvider serviceProvider,
|
||||
IAccountManager accountManager,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
ILogger<BTCPayConnectionManager> logger,
|
||||
BTCPayAppServerClient btcPayAppServerClient,
|
||||
IBTCPayAppHubClient btcPayAppServerClientInterface,
|
||||
ConfigProvider configProvider,
|
||||
SyncService syncService)
|
||||
: BaseHostedService(logger), IHubConnectionObserver
|
||||
{
|
||||
private BTCPayConnectionState _connectionState = BTCPayConnectionState.Init;
|
||||
private CancellationTokenSource _cts = new();
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private IDisposable? _subscription;
|
||||
private IBTCPayAppHubServer? _hubProxy;
|
||||
public IBTCPayAppHubServer? HubProxy
|
||||
{
|
||||
get => Connection?.State == HubConnectionState.Connected ? _hubProxy : null;
|
||||
private set => _hubProxy = value;
|
||||
}
|
||||
private HubConnection? Connection { get; set; }
|
||||
public Network? ReportedNetwork { get; private set; }
|
||||
public string? ReportedNodeInfo { get; set; }
|
||||
private bool ForceSlaveMode { get; set; }
|
||||
public bool RunningInBackground { get; set; }
|
||||
|
||||
public event AsyncEventHandler<(BTCPayConnectionState Old, BTCPayConnectionState New)>? ConnectionChanged;
|
||||
|
||||
public BTCPayConnectionState ConnectionState
|
||||
{
|
||||
get => _connectionState;
|
||||
private set
|
||||
{
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
if (_connectionState == value) return;
|
||||
var old = _connectionState;
|
||||
_connectionState = value;
|
||||
logger.LogInformation("Connection state changed{BgInfo}: {Old} -> {ConnectionState}", BgInfo, old, _connectionState);
|
||||
ConnectionChanged?.Invoke(this, (old, _connectionState));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
ConnectionChanged += OnConnectionChanged;
|
||||
authStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
btcPayAppServerClient.OnNotifyNetwork += OnNotifyNetwork;
|
||||
btcPayAppServerClient.OnNotifyServerEvent += OnNotifyServerEvent;
|
||||
btcPayAppServerClient.OnServerNodeInfo += OnServerNodeInfo;
|
||||
btcPayAppServerClient.OnMasterUpdated += OnMasterUpdated;
|
||||
accountManager.OnEncryptionKeyChanged += OnEncryptionKeyChanged;
|
||||
await OnConnectionChanged(this, (BTCPayConnectionState.Init, BTCPayConnectionState.Init));
|
||||
}
|
||||
|
||||
private async Task OnMasterUpdated(object? sender, long? masterId)
|
||||
{
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
if (masterId is null && ConnectionState == BTCPayConnectionState.ConnectedAsSecondary && !ForceSlaveMode)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Syncing slave {DeviceId}", BgInfo, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
else if (deviceId == masterId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: Setting master to {DeviceId}", BgInfo, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.ConnectedAsPrimary;
|
||||
}
|
||||
else if (ConnectionState == BTCPayConnectionState.ConnectedAsPrimary && masterId != deviceId)
|
||||
{
|
||||
logger.LogInformation("OnMasterUpdated{BgInfo}: New master {MasterId} - Device: {DeviceId}", BgInfo, masterId, deviceId);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task OnEncryptionKeyChanged(object? sender, string encryptionKey)
|
||||
{
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.WaitingForEncryptionKey)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
}
|
||||
}, _cts.Token);
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
}
|
||||
|
||||
private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
var deviceIdentifier = await configProvider.GetDeviceIdentifier();
|
||||
var newState = e.New;
|
||||
try
|
||||
{
|
||||
var account = accountManager.Account;
|
||||
switch (e.New)
|
||||
{
|
||||
case BTCPayConnectionState.Init:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
case BTCPayConnectionState.WaitingForAuth:
|
||||
await syncService.StopSync();
|
||||
if (account is not null && await accountManager.CheckAuthenticated())
|
||||
{
|
||||
newState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.Connecting:
|
||||
if (account is null)
|
||||
{
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
await Kill();
|
||||
var url = new Uri(new Uri(account.BaseUri), "hub/btcpayapp").ToString();
|
||||
var connection = new HubConnectionBuilder()
|
||||
.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(options.PayloadSerializerSettings);
|
||||
options.PayloadSerializerSettings.Converters.Add(new global::BTCPayServer.Lightning.JsonConverters.LightMoneyJsonConverter());
|
||||
})
|
||||
.WithUrl(url, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () =>
|
||||
Task.FromResult(accountManager.Account?.OwnerToken);
|
||||
options.HttpMessageHandlerFactory = serviceProvider
|
||||
.GetService<Func<HttpMessageHandler, HttpMessageHandler>>();
|
||||
options.WebSocketConfiguration =
|
||||
serviceProvider.GetService<Action<ClientWebSocketOptions>>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
_subscription = connection.Register(btcPayAppServerClientInterface);
|
||||
HubProxy = new ExceptionWrappedHubProxy(connection, logger);
|
||||
|
||||
if (connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Closed += OnClosed;
|
||||
connection.Reconnected += OnReconnected;
|
||||
connection.Reconnecting += OnReconnecting;
|
||||
await connection.StartAsync();
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await accountManager.Logout();
|
||||
logger.LogInformation("Signed out user because of unauthorized response{BgInfo}", BgInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (ex is not TaskCanceledException)
|
||||
logger.LogError("Error while connecting to hub{BgInfo}: {Message}", BgInfo, ex.Message);
|
||||
}
|
||||
}
|
||||
Connection = connection;
|
||||
newState = Connection.State switch
|
||||
{
|
||||
HubConnectionState.Connected => BTCPayConnectionState.Syncing,
|
||||
HubConnectionState.Connecting => BTCPayConnectionState.Connecting,
|
||||
_ => BTCPayConnectionState.WaitingForAuth
|
||||
};
|
||||
break;
|
||||
case BTCPayConnectionState.Syncing:
|
||||
await syncService.StopSync();
|
||||
if (await syncService.EncryptionKeyRequiresImport())
|
||||
{
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
logger.LogWarning(
|
||||
"Existing state found but encryption key is missing, waiting until key is provided");
|
||||
}
|
||||
else
|
||||
{
|
||||
//check if we are the master previously to process outbox items
|
||||
var masterDevice = await HubProxy!.GetCurrentMaster();
|
||||
if (deviceIdentifier == masterDevice)
|
||||
{
|
||||
logger.LogInformation("Syncing master to remote{BgInfo}: {DeviceId}", BgInfo, deviceIdentifier);
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Syncing to local{BgInfo}. Master: {MasterId} - Device: {DeviceId}", BgInfo, masterDevice, deviceIdentifier);
|
||||
await syncService.SyncToLocal();
|
||||
}
|
||||
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;
|
||||
|
||||
var config = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key);
|
||||
if (!string.IsNullOrEmpty(config?.CurrentStoreId))
|
||||
{
|
||||
await accountManager.SetCurrentStoreId(config.CurrentStoreId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedFinishedInitialSync:
|
||||
if (ForceSlaveMode)
|
||||
{
|
||||
await HubProxy!.DeviceMasterSignal(deviceIdentifier, false);
|
||||
ForceSlaveMode = false;
|
||||
newState = BTCPayConnectionState.ConnectedAsSecondary;
|
||||
}
|
||||
else if (!await HubProxy!.DeviceMasterSignal(deviceIdentifier, true))
|
||||
{
|
||||
newState = BTCPayConnectionState.ConnectedAsSecondary;
|
||||
}
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsPrimary:
|
||||
await syncService.StartSync(false);
|
||||
break;
|
||||
case BTCPayConnectionState.ConnectedAsSecondary:
|
||||
await syncService.StartSync(true);
|
||||
break;
|
||||
case BTCPayConnectionState.Disconnected:
|
||||
newState = BTCPayConnectionState.WaitingForAuth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Security.Cryptography.CryptographicException ex) when (newState is BTCPayConnectionState.Syncing or BTCPayConnectionState.Connecting)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
newState = BTCPayConnectionState.WaitingForEncryptionKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error while changing connection state from {Old} to {New}{BgInfo}", e.Old, e.New, BgInfo);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = Task.Run(() => ConnectionState = newState);
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnServerNodeInfo(object? sender, string? e)
|
||||
{
|
||||
ReportedNodeInfo = e;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnNotifyServerEvent(object? sender, ServerEvent e)
|
||||
{
|
||||
logger.LogInformation("OnNotifyServerEvent{BgInfo}: {Type} - {Details}", BgInfo, e.Type, e.ToString());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnNotifyNetwork(object? sender, string e)
|
||||
{
|
||||
ReportedNetwork = Network.GetNetwork(e);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
{
|
||||
await WrapInLock(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
var authState = await accountManager.CheckAuthenticated();
|
||||
if (ConnectionState == BTCPayConnectionState.WaitingForAuth && authState)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
}
|
||||
else if (ConnectionState > BTCPayConnectionState.WaitingForAuth && !authState)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.WaitingForAuth;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while handling authentication state change{BgInfo}", BgInfo);
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task Kill()
|
||||
{
|
||||
if (Connection is not null)
|
||||
{
|
||||
logger.LogWarning("Killing connection{BgInfo}", BgInfo);
|
||||
}
|
||||
var conn = Connection;
|
||||
Connection = null;
|
||||
if (conn is not null)
|
||||
{
|
||||
conn.Closed -= OnClosed;
|
||||
conn.Reconnected -= OnReconnected;
|
||||
conn.Reconnecting -= OnReconnecting;
|
||||
|
||||
await conn.StopAsync();
|
||||
}
|
||||
_subscription?.Dispose();
|
||||
_subscription = null;
|
||||
HubProxy = null;
|
||||
await syncService.StopSync();
|
||||
}
|
||||
|
||||
protected override async Task ExecuteStopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}{BgInfo}", deviceId, BgInfo);
|
||||
await syncService.StopSync();
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
if (HubProxy is not null)
|
||||
{
|
||||
await HubProxy.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
await Kill();
|
||||
authStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
btcPayAppServerClient.OnNotifyNetwork -= OnNotifyNetwork;
|
||||
accountManager.OnEncryptionKeyChanged -= OnEncryptionKeyChanged;
|
||||
ConnectionChanged -= OnConnectionChanged;
|
||||
}
|
||||
|
||||
public Task OnClosed(Exception? ex)
|
||||
{
|
||||
logger.LogError("Hub connection closed{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
if (Connection?.State == HubConnectionState.Disconnected && ConnectionState != BTCPayConnectionState.Connecting)
|
||||
{
|
||||
ConnectionState = BTCPayConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnected(string? connectionId)
|
||||
{
|
||||
logger.LogInformation("Hub connection reconnected{BgInfo}", BgInfo);
|
||||
ConnectionState = BTCPayConnectionState.Syncing;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnReconnecting(Exception? ex)
|
||||
{
|
||||
logger.LogWarning("Hub connection reconnecting{BgInfo}: {Message}", BgInfo, ex?.Message);
|
||||
ConnectionState = BTCPayConnectionState.Connecting;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SwitchToSecondary()
|
||||
{
|
||||
if (_connectionState == BTCPayConnectionState.ConnectedAsPrimary)
|
||||
{
|
||||
ForceSlaveMode = true;
|
||||
var deviceId = await configProvider.GetDeviceIdentifier();
|
||||
logger.LogInformation("Sending device master signal to turn off {DeviceId}", deviceId);
|
||||
await syncService.StopSync();
|
||||
await syncService.SyncToRemote(CancellationToken.None);
|
||||
await HubProxy!.DeviceMasterSignal(deviceId, false);
|
||||
}
|
||||
}
|
||||
|
||||
private string BgInfo => RunningInBackground ? " (in background mode)" : string.Empty;
|
||||
}
|
||||
17
BTCPayApp.Core/BTCPayServer/BTCPayConnectionState.cs
Normal file
17
BTCPayApp.Core/BTCPayServer/BTCPayConnectionState.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BTCPayConnectionState
|
||||
{
|
||||
Init,
|
||||
Disconnected,
|
||||
WaitingForAuth,
|
||||
Connecting,
|
||||
Syncing,
|
||||
WaitingForEncryptionKey,
|
||||
ConnectedAsPrimary,
|
||||
ConnectedAsSecondary,
|
||||
ConnectedFinishedInitialSync
|
||||
}
|
||||
43
BTCPayApp.Core/BTCPayServer/BTCPayPaymentsNotifier.cs
Normal file
43
BTCPayApp.Core/BTCPayServer/BTCPayPaymentsNotifier.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class BTCPayPaymentsNotifier(
|
||||
PaymentsManager paymentsManager,
|
||||
BTCPayConnectionManager connectionManager)
|
||||
: IScopedHostedService
|
||||
{
|
||||
private bool _listening;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
paymentsManager.OnPaymentUpdate += OnPaymentUpdate;
|
||||
connectionManager.ConnectionChanged += ConnectionManagerOnConnectionChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task ConnectionManagerOnConnectionChanged(object? sender, (BTCPayConnectionState Old, BTCPayConnectionState New) e)
|
||||
{
|
||||
_listening = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnPaymentUpdate(object? sender, AppLightningPayment e)
|
||||
{
|
||||
if (!_listening || connectionManager.HubProxy is null) return;
|
||||
await connectionManager.HubProxy.SendInvoiceUpdate(e.ToInvoice());
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
paymentsManager.OnPaymentUpdate -= OnPaymentUpdate;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StartListen()
|
||||
{
|
||||
_listening = true;
|
||||
}
|
||||
}
|
||||
112
BTCPayApp.Core/BTCPayServer/ExceptionWrappedHubProxy.cs
Normal file
112
BTCPayApp.Core/BTCPayServer/ExceptionWrappedHubProxy.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TypedSignalR.Client;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
public class ExceptionWrappedHubProxy : IBTCPayAppHubServer
|
||||
{
|
||||
private readonly IBTCPayAppHubServer _hubProxy;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ExceptionWrappedHubProxy(HubConnection connection, ILogger logger)
|
||||
{
|
||||
_hubProxy = connection.CreateHubProxy<IBTCPayAppHubServer>();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<T> Wrap<T>(Func<Task<T>> func)
|
||||
{
|
||||
return await AsyncExtensions.RunInOtherThread(async () =>
|
||||
{
|
||||
// executes in thread pool
|
||||
try
|
||||
{
|
||||
return await func();
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
_logger.LogError(e, $"Error while calling hub method");
|
||||
return default!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while calling hub method");
|
||||
return default!;
|
||||
|
||||
}
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
public async Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.DeviceMasterSignal(deviceIdentifier, active));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> Pair(PairRequest request)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.Pair(request));
|
||||
}
|
||||
|
||||
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.Handshake(request));
|
||||
}
|
||||
|
||||
public async Task<bool> BroadcastTransaction(string tx)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.BroadcastTransaction(tx));
|
||||
}
|
||||
|
||||
public async Task<decimal> GetFeeRate(int blockTarget)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetFeeRate(blockTarget));
|
||||
}
|
||||
|
||||
public async Task<BestBlockResponse?> GetBestBlock()
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetBestBlock());
|
||||
}
|
||||
|
||||
public async Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.FetchTxsAndTheirBlockHeads(identifier, txIds, outpoints));
|
||||
}
|
||||
|
||||
public async Task<ScriptResponse> DeriveScript(string identifier)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.DeriveScript(identifier));
|
||||
}
|
||||
|
||||
public async Task TrackScripts(string identifier, string[] scripts)
|
||||
{
|
||||
await Wrap(() => Task.FromResult(_hubProxy.TrackScripts(identifier, scripts)));
|
||||
}
|
||||
|
||||
public async Task<string> UpdatePsbt(string[] identifiers, string psbt)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.UpdatePsbt(identifiers, psbt));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, CoinResponse[]>> GetUTXOs(string[] identifiers)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetUTXOs(identifiers));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers)
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetTransactions(identifiers));
|
||||
}
|
||||
|
||||
public async Task SendInvoiceUpdate(LightningInvoice lightningInvoice)
|
||||
{
|
||||
await Wrap(() => Task.FromResult(_hubProxy.SendInvoiceUpdate(lightningInvoice)));
|
||||
}
|
||||
|
||||
public async Task<long?> GetCurrentMaster()
|
||||
{
|
||||
return await Wrap(async () => await _hubProxy.GetCurrentMaster());
|
||||
}
|
||||
}
|
||||
137
BTCPayApp.Core/BTCPayServer/IBTCPayAppHubClient.cs
Normal file
137
BTCPayApp.Core/BTCPayServer/IBTCPayAppHubClient.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
|
||||
//methods available on the hub in the client
|
||||
public interface IBTCPayAppHubClient
|
||||
{
|
||||
Task NotifyServerEvent(ServerEvent ev);
|
||||
Task NotifyNetwork(string network);
|
||||
Task NotifyServerNode(string nodeInfo);
|
||||
Task TransactionDetected(TransactionDetectedRequest request);
|
||||
Task NewBlock(string block);
|
||||
Task StartListen(string key);
|
||||
|
||||
Task<LightningInvoice> CreateInvoice(string key, CreateLightningInvoiceRequest createLightningInvoiceRequest);
|
||||
Task<LightningInvoice?> GetLightningInvoice(string key, uint256 paymentHash);
|
||||
Task<LightningPayment?> GetLightningPayment(string key, uint256 paymentHash);
|
||||
Task CancelInvoice(string key, uint256 paymentHash);
|
||||
Task<List<LightningPayment>> GetLightningPayments(string key, ListPaymentsParams request);
|
||||
Task<List<LightningInvoice>> GetLightningInvoices(string key, ListInvoicesParams request);
|
||||
Task<PayResponse> PayInvoice(string key, string bolt11, long? amountMilliSatoshi);
|
||||
Task MasterUpdated(long? deviceIdentifier);
|
||||
Task<LightningNodeInformation> GetLightningNodeInfo(string key);
|
||||
Task<LightningNodeBalance> GetLightningBalance(string key);
|
||||
}
|
||||
|
||||
//methods available on the hub in the server
|
||||
public interface IBTCPayAppHubServer
|
||||
{
|
||||
Task<bool> DeviceMasterSignal(long deviceIdentifier, bool active);
|
||||
Task<Dictionary<string,string>> Pair(PairRequest request);
|
||||
Task<AppHandshakeResponse> Handshake(AppHandshake request);
|
||||
Task<bool> BroadcastTransaction(string tx);
|
||||
Task<decimal> GetFeeRate(int blockTarget);
|
||||
Task<BestBlockResponse?> GetBestBlock();
|
||||
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string identifier, string[] txIds, string[] outpoints);
|
||||
Task<ScriptResponse> DeriveScript(string identifier);
|
||||
Task TrackScripts(string identifier, string[] scripts);
|
||||
Task<string> UpdatePsbt(string[] identifiers, string psbt);
|
||||
Task<Dictionary<string, CoinResponse[]>> GetUTXOs(string[] identifiers);
|
||||
Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers);
|
||||
Task SendInvoiceUpdate(LightningInvoice lightningInvoice);
|
||||
Task<long?> GetCurrentMaster();
|
||||
}
|
||||
|
||||
public class ServerEvent
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public string? StoreId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? AppId { get; set; }
|
||||
public string? InvoiceId { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
public record TxResp
|
||||
{
|
||||
public string TransactionId { get; set; } = null!;
|
||||
public long Confirmations { get; set; }
|
||||
public long? Height { get; set; }
|
||||
public decimal BalanceChange { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{ Confirmations = {Confirmations}, Height = {Height}, BalanceChange = {BalanceChange}, Timestamp = {Timestamp}, TransactionId = {TransactionId} }}";
|
||||
}
|
||||
}
|
||||
|
||||
public class TransactionDetectedRequest
|
||||
{
|
||||
public string? Identifier { get; set; }
|
||||
public string? TxId { get; set; }
|
||||
public string[]? SpentScripts { get; set; }
|
||||
public string[]? ReceivedScripts { get; set; }
|
||||
public bool Confirmed { get; set; }
|
||||
}
|
||||
|
||||
public class CoinResponse
|
||||
{
|
||||
public bool Confirmed { get; set; }
|
||||
public string? Script { get; set; }
|
||||
public string? Outpoint { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
public class TxInfoResponse
|
||||
{
|
||||
public Dictionary<string,TransactionResponse>? Txs { get; set; }
|
||||
public Dictionary<string,string>? BlockHeaders { get; set; }
|
||||
public Dictionary<string,int>? BlockHeights { get; set; }
|
||||
}
|
||||
|
||||
public class TransactionResponse
|
||||
{
|
||||
public string? BlockHash { get; set; }
|
||||
public string? Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class BestBlockResponse
|
||||
{
|
||||
public string? BlockHash { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string? BlockHeader { get; set; }
|
||||
}
|
||||
|
||||
public class ScriptResponse
|
||||
{
|
||||
public string Script { get; set; } = null!;
|
||||
public string KeyPath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class AppHandshake
|
||||
{
|
||||
public string[]? Identifiers { get; set; }
|
||||
}
|
||||
|
||||
//response about identifiers being tracked successfully
|
||||
public class AppHandshakeResponse
|
||||
{
|
||||
public string[]? IdentifiersAcknowledged { get; set; }
|
||||
}
|
||||
|
||||
public class PairRequest
|
||||
{
|
||||
public Dictionary<string, DerivationItem> Derivations { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DerivationItem
|
||||
{
|
||||
public string? Descriptor { get; set; }
|
||||
public int Index { get; set; }
|
||||
public OutPoint[] KnownCoins { get; set; } = [];
|
||||
}
|
||||
52
BTCPayApp.Core/Backup/SingleKeyDataProtector.cs
Normal file
52
BTCPayApp.Core/Backup/SingleKeyDataProtector.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
|
||||
public class SingleKeyDataProtector : IDataProtector
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
|
||||
public SingleKeyDataProtector(byte[] key)
|
||||
{
|
||||
if (key.Length != 32) // AES-256 key size
|
||||
throw new ArgumentException("Key length must be 32 bytes.");
|
||||
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public IDataProtector CreateProtector(string purpose)
|
||||
{
|
||||
using var hmac = new HMACSHA256(_key);
|
||||
var purposeBytes = Encoding.UTF8.GetBytes(purpose);
|
||||
var key = hmac.ComputeHash(purposeBytes).Take(32).ToArray();
|
||||
return new SingleKeyDataProtector(key);
|
||||
}
|
||||
|
||||
public byte[] Protect(byte[] plaintext)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.GenerateIV();
|
||||
|
||||
var iv = aes.IV;
|
||||
var encrypted = aes.EncryptCbc(plaintext, iv);
|
||||
|
||||
return iv.Concat(encrypted).ToArray();
|
||||
}
|
||||
|
||||
public byte[] Unprotect(byte[] protectedData)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
if (protectedData.Length == 0)
|
||||
return protectedData;
|
||||
|
||||
var iv = protectedData.Take(16).ToArray();
|
||||
var cipherText = protectedData.Skip(16).ToArray();
|
||||
|
||||
return aes.DecryptCbc(cipherText, iv);
|
||||
}
|
||||
}
|
||||
423
BTCPayApp.Core/Backup/SyncService.cs
Normal file
423
BTCPayApp.Core/Backup/SyncService.cs
Normal file
@ -0,0 +1,423 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using VSS;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayApp.Core.Backup;
|
||||
|
||||
public class SyncService(
|
||||
ConfigProvider configProvider,
|
||||
ILogger<SyncService> logger,
|
||||
IAccountManager accountManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
: IDisposable
|
||||
{
|
||||
public AsyncEventHandler<(List<Outbox> OutboxItemsProcesed, PutObjectRequest RemoteRequest)>? RemoteObjectUpdated;
|
||||
public AsyncEventHandler<string[]>? LocalUpdated;
|
||||
private (Task syncTask, CancellationTokenSource cts, bool local)? _syncTask;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
|
||||
private async Task<IDataProtector?> GetDataProtector()
|
||||
{
|
||||
var key = await accountManager.GetEncryptionKey();
|
||||
return string.IsNullOrEmpty(key) ? null : new SingleKeyDataProtector(Convert.FromHexString(key));
|
||||
}
|
||||
|
||||
public async Task<bool> EncryptionKeyRequiresImport()
|
||||
{
|
||||
var dataProtector = await GetDataProtector();
|
||||
if (dataProtector is not null)
|
||||
return false;
|
||||
|
||||
var api = await GetUnencryptedVSSAPI();
|
||||
try
|
||||
{
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
|
||||
if (res.Value is null or {Value.Length: 0})
|
||||
return false;
|
||||
|
||||
if (dataProtector is null)
|
||||
return true;
|
||||
|
||||
var decrypted = dataProtector.Unprotect(res.Value.ToByteArray());
|
||||
return "kukks" == Encoding.UTF8.GetString(decrypted);
|
||||
}
|
||||
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while checking if encryption key requires import");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SetEncryptionKey(Mnemonic mnemonic)
|
||||
{
|
||||
var key = mnemonic.DeriveExtKey().Derive(1337).PrivateKey.ToBytes();
|
||||
return await SetEncryptionKey(Convert.ToHexString(key));
|
||||
}
|
||||
|
||||
public async Task<bool> SetEncryptionKey(string key)
|
||||
{
|
||||
if (key.Contains(' ')) return await SetEncryptionKey(new Mnemonic(key));
|
||||
|
||||
var dataProtector = new SingleKeyDataProtector(Convert.FromHexString(key));
|
||||
var encrypted = dataProtector.Protect("kukks"u8.ToArray());
|
||||
var api = await GetUnencryptedVSSAPI();
|
||||
|
||||
try
|
||||
{
|
||||
var res = await api.GetObjectAsync(new GetObjectRequest
|
||||
{
|
||||
Key = "encryptionKeyTest"
|
||||
});
|
||||
|
||||
if (res.Value is {Value.Length: > 0})
|
||||
{
|
||||
var decrypted = dataProtector.Unprotect(res.Value.Value.ToByteArray());
|
||||
if ("kukks" == Encoding.UTF8.GetString(decrypted))
|
||||
{
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (VSSClientException e) when (e.Error.ErrorCode == ErrorCode.NoSuchKeyException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Error while setting encryption key: {Message}", e.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
await api.PutObjectAsync(new PutObjectRequest
|
||||
{
|
||||
GlobalVersion = await configProvider.GetDeviceIdentifier(),
|
||||
TransactionItems =
|
||||
{
|
||||
new KeyValue
|
||||
{
|
||||
Key = "encryptionKeyTest",
|
||||
Value = ByteString.CopyFrom(encrypted)
|
||||
}
|
||||
},
|
||||
});
|
||||
await accountManager.SetEncryptionKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task<IVSSAPI> GetUnencryptedVSSAPI()
|
||||
{
|
||||
var account = accountManager.Account;
|
||||
if (account is null)
|
||||
throw new InvalidOperationException("Account not found");
|
||||
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
|
||||
var httpClient = httpClientFactory.CreateClient("vss");
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", account.OwnerToken);
|
||||
var vssClient = new HttpVSSAPIClient(vssUri, httpClient);
|
||||
return Task.FromResult<IVSSAPI>(vssClient);
|
||||
}
|
||||
|
||||
private async Task<IVSSAPI?> GetVSSAPI()
|
||||
{
|
||||
var dataProtector = await GetDataProtector();
|
||||
return dataProtector is null ? null : new VSSApiEncryptorClient(await GetUnencryptedVSSAPI(), dataProtector);
|
||||
}
|
||||
|
||||
private static async Task<KeyValue[]> CreateLocalVersions(AppDbContext dbContext)
|
||||
{
|
||||
var settings = dbContext.Settings.Where(setting => setting.Backup).Select(setting => new KeyValue
|
||||
{
|
||||
Key = setting.EntityKey,
|
||||
Version = setting.Version
|
||||
});
|
||||
var channels = dbContext.LightningChannels.Select(channel => new KeyValue
|
||||
{
|
||||
Key = channel.EntityKey,
|
||||
Version = channel.Version
|
||||
});
|
||||
var payments = dbContext.LightningPayments.Select(payment => new KeyValue
|
||||
{
|
||||
Key = payment.EntityKey,
|
||||
Version = payment.Version
|
||||
});
|
||||
return await settings.Concat(channels).Concat(payments).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task SyncToLocal(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backupApi = await GetVSSAPI();
|
||||
if (backupApi is null)
|
||||
return;
|
||||
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var localVersions = await CreateLocalVersions(db);
|
||||
|
||||
var remoteVersions = await backupApi.ListKeyVersionsAsync(new ListKeyVersionsRequest(), cancellationToken);
|
||||
await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var triggers = await db.Database
|
||||
.SqlQuery<TriggerRecord>($"SELECT name, sql FROM sqlite_master WHERE type = 'trigger'")
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
string.Join("; ", triggers.Select(trigger => $"DROP TRIGGER IF EXISTS {trigger.name}")),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
// delete local versions that are not in remote
|
||||
// delete local versions which are lower than remote
|
||||
|
||||
var toDelete = localVersions.Where(localVersion =>
|
||||
remoteVersions.KeyVersions.All(remoteVersion => remoteVersion.Key != localVersion.Key)
|
||||
|| remoteVersions.KeyVersions.All(remoteVersion =>
|
||||
remoteVersion.Key == localVersion.Key && remoteVersion.Version > localVersion.Version)).ToArray();
|
||||
|
||||
var toUpsert = remoteVersions.KeyVersions.Where(remoteVersion => localVersions.All(localVersion =>
|
||||
localVersion.Key != remoteVersion.Key || localVersion.Version < remoteVersion.Version)).Where(value => value.Key != "encryptionKeyTest").ToArray();
|
||||
|
||||
if (toDelete.Length == 0 && toUpsert.Length == 0)
|
||||
return;
|
||||
logger.LogInformation("Syncing to local: {ToDelete} to delete, {ToUpsert} to upsert", toDelete.Length,
|
||||
toUpsert.Length);
|
||||
|
||||
foreach (var upsertItem in toUpsert)
|
||||
{
|
||||
if (upsertItem.Value is not (null or { Length: 0 })) continue;
|
||||
var item = await backupApi.GetObjectAsync(new GetObjectRequest()
|
||||
{
|
||||
Key = upsertItem.Key,
|
||||
}, cancellationToken);
|
||||
upsertItem.MergeFrom(item.Value);
|
||||
}
|
||||
|
||||
var settingsToDelete = toDelete.Where(key => key.Key.StartsWith("Setting_")).Select(key => key.Key);
|
||||
var channelsToDelete = toDelete.Where(key => key.Key.StartsWith("Channel_")).Select(key => key.Key);
|
||||
var paymentsToDelete = toDelete.Where(key => key.Key.StartsWith("Payment_")).Select(key => key.Key);
|
||||
var deleteCount = 0;
|
||||
deleteCount += await db.Settings.Where(setting => settingsToDelete.Contains(setting.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
deleteCount += await db.LightningChannels.Where(channel => channelsToDelete.Contains(channel.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
deleteCount += await db.LightningPayments.Where(payment => paymentsToDelete.Contains(payment.EntityKey))
|
||||
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
|
||||
|
||||
// upsert the rest when needed
|
||||
var settingsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Setting_")).Select(setting => new Setting()
|
||||
{
|
||||
Key = setting.Key.Replace("Setting_", ""),
|
||||
Value = setting.Value.ToByteArray(),
|
||||
Version = setting.Version,
|
||||
Backup = true
|
||||
}).ToArray();
|
||||
var channelsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Channel_"))
|
||||
.Select(value => JsonSerializer.Deserialize<Channel>(value.Value.ToStringUtf8())!);
|
||||
var paymentsToUpsert = toUpsert.Where(key => key.Key.StartsWith("Payment_")).Select(value =>
|
||||
JsonSerializer.Deserialize<AppLightningPayment>(value.Value.ToStringUtf8())!);
|
||||
var upsertCount = 0;
|
||||
upsertCount += await db.Settings.UpsertRange(settingsToUpsert).On(setting => setting.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
upsertCount += await db.LightningChannels.UpsertRange(channelsToUpsert).On(channel => channel.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
upsertCount += await db.LightningPayments.UpsertRange(paymentsToUpsert).On(payment => payment.EntityKey)
|
||||
.RunAsync(cancellationToken);
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync(string.Join("; ", triggers.Select(record => record.sql)),
|
||||
cancellationToken: cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.Database.CommitTransactionAsync(cancellationToken);
|
||||
logger.LogInformation("Synced to local: {DeleteCount} deleted, {UpsertCount} upserted", deleteCount,
|
||||
upsertCount);
|
||||
LocalUpdated?.Invoke(this, toDelete.Concat(toUpsert).Select(key => key.Key).ToArray());
|
||||
settingsToUpsert.Select(setting => setting.Key).Concat(settingsToDelete).Distinct().ToList()
|
||||
.ForEach(key => configProvider.Updated?.Invoke(this, key));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await db.Database.RollbackTransactionAsync(cancellationToken);
|
||||
logger.LogError(e, "Error while syncing to local");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<KeyValue?> GetValue(AppDbContext dbContext, Outbox outbox)
|
||||
{
|
||||
switch (outbox.Entity)
|
||||
{
|
||||
case "Setting":
|
||||
var setting = await dbContext.Settings.SingleOrDefaultAsync(setting1 =>
|
||||
setting1.EntityKey == outbox.Key && setting1.Backup);
|
||||
if (setting == null)
|
||||
return null;
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(setting.Value),
|
||||
Version = setting.Version
|
||||
};
|
||||
case "Channel":
|
||||
var channel = await dbContext.LightningChannels.Include(channel1 => channel1.Aliases)
|
||||
.SingleOrDefaultAsync(channel1 => channel1.EntityKey == outbox.Key);
|
||||
|
||||
if (channel == null)
|
||||
return null;
|
||||
var val = JsonSerializer.SerializeToUtf8Bytes(channel);
|
||||
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(val),
|
||||
Version = channel.Version
|
||||
};
|
||||
case "Payment":
|
||||
var payment = await dbContext.LightningPayments.SingleOrDefaultAsync(lightningPayment =>
|
||||
lightningPayment.EntityKey == outbox.Key);
|
||||
if (payment == null)
|
||||
return null;
|
||||
var paymentBytes = JsonSerializer.SerializeToUtf8Bytes(payment);
|
||||
return new KeyValue
|
||||
{
|
||||
Key = outbox.Key,
|
||||
Value = ByteString.CopyFrom(paymentBytes),
|
||||
Version = payment.Version
|
||||
};
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SyncToRemote(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _syncLock.WaitAsync(cancellationToken);
|
||||
|
||||
var backupAPi = await GetVSSAPI();
|
||||
if (backupAPi is null)
|
||||
return;
|
||||
await using var db = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var putObjectRequest = new PutObjectRequest
|
||||
{
|
||||
GlobalVersion = await configProvider.GetDeviceIdentifier()
|
||||
};
|
||||
var outbox = await db.OutboxItems.GroupBy(outbox1 => outbox1.Key)
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
if (outbox.Count != 0)
|
||||
{
|
||||
logger.LogInformation("Syncing to remote {Count} outbox items", outbox.Count);
|
||||
}
|
||||
var removedOutboxItems = new List<Outbox>();
|
||||
foreach (var outboxItemSet in outbox)
|
||||
{
|
||||
var orderedEnumerable = outboxItemSet.OrderByDescending(outbox1 => outbox1.Version)
|
||||
.ThenByDescending(outbox1 => outbox1.ActionType).ToArray();
|
||||
foreach (var item in orderedEnumerable)
|
||||
{
|
||||
if (item.ActionType == OutboxAction.Delete)
|
||||
{
|
||||
putObjectRequest.DeleteItems.Add(new KeyValue()
|
||||
{
|
||||
Key = item.Key, Version = item.Version
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var kv = await GetValue(db, item);
|
||||
if (kv != null)
|
||||
{
|
||||
putObjectRequest.TransactionItems.Add(kv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.OutboxItems.RemoveRange(orderedEnumerable);
|
||||
removedOutboxItems.AddRange(orderedEnumerable);
|
||||
// Process outbox item
|
||||
}
|
||||
|
||||
if (putObjectRequest.TransactionItems.Count == 0 && putObjectRequest.DeleteItems.Count == 0 && _syncTask is not null) return;
|
||||
|
||||
await backupAPi.PutObjectAsync(putObjectRequest, cancellationToken);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Synced to remote {TransactionItemsCount} items and deleted {DeleteItemsCount} items {Join}",
|
||||
putObjectRequest.TransactionItems.Count,
|
||||
putObjectRequest.DeleteItems.Count,
|
||||
string.Join(", ", putObjectRequest.TransactionItems.Select(kv => kv.Key + " " + kv.Version)));
|
||||
RemoteObjectUpdated?.Invoke(this, (removedOutboxItems, putObjectRequest.Clone()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
}
|
||||
public async Task StartSync(bool local,CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_syncTask.HasValue && _syncTask.Value.local == local && !_syncTask.Value.cts.IsCancellationRequested)
|
||||
return;
|
||||
if (_syncTask.HasValue && _syncTask.Value.local != local)
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_syncTask = (ContinuouslySync(local, cts.Token), cts, local);
|
||||
}
|
||||
|
||||
public async Task StopSync()
|
||||
{
|
||||
if (_syncTask.HasValue)
|
||||
{
|
||||
await _syncTask.Value.cts.CancelAsync();
|
||||
_syncTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ContinuouslySync(bool local, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (local)
|
||||
await SyncToLocal(cancellationToken);
|
||||
else
|
||||
await SyncToRemote(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while syncing to {Target}", local ? "local" : "remote");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RemoteObjectUpdated = null;
|
||||
LocalUpdated = null;
|
||||
}
|
||||
}
|
||||
9
BTCPayApp.Core/Constants.cs
Normal file
9
BTCPayApp.Core/Constants.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const string LoginCodeSeparator = ";";
|
||||
public const string EncryptionKeySeparator = "*";
|
||||
public const string InviteSeparator = "/invite/";
|
||||
public const string POSQRLoginSeparator = "loginCode";
|
||||
}
|
||||
@ -1,8 +1,16 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
|
||||
public interface IConfigProvider
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public abstract class ConfigProvider : IDisposable
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
Task<IEnumerable<string>> List(string prefix);
|
||||
public abstract Task<T?> Get<T>(string key);
|
||||
public abstract Task Set<T>(string key, T? value, bool backup);
|
||||
public abstract Task<IEnumerable<string>> List(string prefix);
|
||||
public AsyncEventHandler<string>? Updated;
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Updated = null;
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,5 @@
|
||||
public interface IDataDirectoryProvider
|
||||
{
|
||||
Task<string> GetAppDataDirectory();
|
||||
}
|
||||
Task<string> GetCacheDirectory();
|
||||
}
|
||||
|
||||
7
BTCPayApp.Core/Contracts/IEmailService.cs
Normal file
7
BTCPayApp.Core/Contracts/IEmailService.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayApp.Core.Contracts
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendAsync(string subject, string body, string recipient, string? attachFilePath = null);
|
||||
}
|
||||
}
|
||||
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
public interface INfcService: IDisposable
|
||||
{
|
||||
event EventHandler<NfcCardData> OnNfcDataReceived;
|
||||
void StartNfc();
|
||||
void EndNfc();
|
||||
}
|
||||
|
||||
public class NfcCardData
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public byte[] Payload { get; set; }
|
||||
}
|
||||
@ -1,3 +1,7 @@
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
public interface ISecureConfigProvider : IConfigProvider;
|
||||
public interface ISecureConfigProvider
|
||||
{
|
||||
Task<T?> Get<T>(string key);
|
||||
Task Set<T>(string key, T? value);
|
||||
}
|
||||
|
||||
12
BTCPayApp.Core/Contracts/SetupState.cs
Normal file
12
BTCPayApp.Core/Contracts/SetupState.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SetupState
|
||||
{
|
||||
Undetermined,
|
||||
Pending,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@ -1,40 +1,196 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.CommonServer.Models;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Laraue.EfCoreTriggers.Common.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Setting> Settings { get; set; }
|
||||
|
||||
public DbSet<Channel> LightningChannels { get; set; }
|
||||
public DbSet<LightningPayment> LightningPayments { get; set; }
|
||||
// public DbSet<SpendableCoin> SpendableCoins { get; set; }
|
||||
|
||||
public DbSet<ChannelAlias> ChannelAliases { get; set; }
|
||||
public DbSet<AppLightningPayment> LightningPayments { get; set; }
|
||||
public DbSet<Outbox> OutboxItems { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//TODO: add paymentId to the primary key and generate a random one if not provided
|
||||
modelBuilder.Entity<LightningPayment>()
|
||||
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
|
||||
//we use system.text.json because it is natively supported in efcore for querying
|
||||
modelBuilder.Entity<LightningPayment>().Property(p => p.AdditionalData)
|
||||
modelBuilder.Entity<Outbox>()
|
||||
.HasKey(w => new {w.Entity, w.Key, w.ActionType, w.Version});
|
||||
modelBuilder.Entity<Outbox>().Property(payment => payment.Timestamp).HasDefaultValueSql("datetime('now')");
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<Setting>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<Channel>().HasIndex(payment => payment.EntityKey).IsUnique();
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentRequest)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
|
||||
v => JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(v, JsonSerializerOptions.Default)!);
|
||||
request => request!.ToString(),
|
||||
str => NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network)));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Secret)
|
||||
.HasConversion(
|
||||
request => request!.ToString(),
|
||||
str => uint256.Parse(str));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.PaymentHash)
|
||||
.HasConversion(
|
||||
request => request!.ToString(),
|
||||
str => uint256.Parse(str));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.Value)
|
||||
.HasConversion(
|
||||
request => request!.MilliSatoshi,
|
||||
str => new LightMoney(str));
|
||||
|
||||
modelBuilder.Entity<Channel>().Property(channel => channel.AdditionalData).HasJsonConversion();
|
||||
modelBuilder.Entity<AppLightningPayment>().Property(payment => payment.AdditionalData).HasJsonConversion();
|
||||
modelBuilder.Entity<AppLightningPayment>()
|
||||
.HasKey(w => new {w.PaymentHash, w.Inbound, w.PaymentId});
|
||||
|
||||
|
||||
//handling versioned data
|
||||
|
||||
//settings, channels, payments
|
||||
|
||||
//when creating, set the version to 0
|
||||
//when updating, increment the version
|
||||
|
||||
// outbox creation
|
||||
// when creating, insert an outbox item
|
||||
// when updating, insert an outbox item
|
||||
// when deleting, insert an outbox item
|
||||
|
||||
modelBuilder.Entity<Setting>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group =>
|
||||
{
|
||||
group
|
||||
.Condition(@ref => @ref.New.Backup)
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Insert
|
||||
});
|
||||
}))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Condition(@ref => @ref.Old.Backup)
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Setting" && outbox.Key == @ref.Old.Key,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.EntityKey,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group
|
||||
.Condition(@ref => @ref.Old.Backup)
|
||||
// .Condition(@ref => @ref.Old.Value != @ref.New.Value)
|
||||
.Update<Setting>(
|
||||
(tableRefs, setting) => tableRefs.Old.Key == setting.Key,
|
||||
(tableRefs, setting) => new Setting { Key = tableRefs.Old.Key, Version = tableRefs.Old.Version + 1 })
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Setting",
|
||||
Version = @ref.Old.Version + 1,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
// .Action(group => group
|
||||
// .Condition(@ref => @ref.Old.Backup && !@ref.New.Backup)
|
||||
// .Insert(
|
||||
// // .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Setting" && outbox.Key == @ref.New.Key,
|
||||
// @ref => new Outbox()
|
||||
// {
|
||||
// Entity = "Setting",
|
||||
// Version = @ref.Old.Version +1,
|
||||
// Key = @ref.New.Key,
|
||||
// ActionType = OutboxAction.Delete
|
||||
// })));
|
||||
|
||||
modelBuilder.Entity<Channel>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Channel" && outbox.Key == @ref.Old.Id,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.EntityKey,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group => group.Update<Channel>(
|
||||
(tableRefs, setting) => tableRefs.Old.Id == setting.Id,
|
||||
(tableRefs, setting) => new Channel { Id = tableRefs.Old.Id, Version = tableRefs.Old.Version + 1 }).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.New.Version == outbox.Version && outbox.ActionType == OutboxAction.Update && outbox.Entity == "Channel" && outbox.Key == @ref.New.Id,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Channel",
|
||||
Version = @ref.Old.Version +1,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
|
||||
modelBuilder.Entity<AppLightningPayment>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => outbox.Version == @ref.New.Version && outbox.ActionType == OutboxAction.Insert && outbox.Entity == "Payment" && outbox.Key == @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.New.Version,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Insert
|
||||
})))
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(group => group
|
||||
.Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) => @ref.Old.Version == outbox.Version && outbox.ActionType == OutboxAction.Delete && outbox.Entity == "Payment" && outbox.Key == @ref.Old.PaymentHash+ "_"+@ref.Old.PaymentId+ "_"+@ref.Old.Inbound,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version,
|
||||
Key = @ref.Old.EntityKey,
|
||||
ActionType = OutboxAction.Delete
|
||||
})))
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(group =>
|
||||
|
||||
group.Update<AppLightningPayment>(
|
||||
(tableRefs, setting) => tableRefs.Old.PaymentHash == setting.PaymentHash,
|
||||
(tableRefs, setting) => new AppLightningPayment {Version = tableRefs.Old.Version + 1}).Insert(
|
||||
// .InsertIfNotExists( (@ref, outbox) =>
|
||||
// outbox.Version != @ref.New.Version || outbox.ActionType != OutboxAction.Update || outbox.Entity != "Payment" || outbox.Key != @ref.New.PaymentHash+ "_"+@ref.New.PaymentId+ "_"+@ref.New.Inbound,
|
||||
@ref => new Outbox
|
||||
{
|
||||
Entity = "Payment",
|
||||
Version = @ref.Old.Version +1,
|
||||
Key = @ref.New.EntityKey,
|
||||
ActionType = OutboxAction.Update
|
||||
})));
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
public class SpendableCoin
|
||||
{
|
||||
public string Script { get; set; }
|
||||
[Key] public string Outpoint { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
}
|
||||
41
BTCPayApp.Core/Data/AppLightningPayment.cs
Normal file
41
BTCPayApp.Core/Data/AppLightningPayment.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class AppLightningPayment : VersionedData
|
||||
{
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256? PaymentHash { get; set; }
|
||||
|
||||
public string? PaymentId { get; set; }
|
||||
public string? Preimage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256? Secret { get; set; }
|
||||
|
||||
public bool Inbound { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney? Value { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LightningPaymentStatus Status { get; set; }
|
||||
|
||||
[JsonConverter(typeof(BOLT11PaymentRequestJsonConverter))]
|
||||
public BOLT11PaymentRequest? PaymentRequest { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
|
||||
|
||||
public override string EntityKey
|
||||
{
|
||||
get => $"Payment_{PaymentHash}_{PaymentId}_{Inbound}";
|
||||
init { }
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,32 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class Channel
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Channel:VersionedData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public List<string> Aliases { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
|
||||
public required string Id { get; init; }
|
||||
public byte[]? Data { get; set; }
|
||||
public List<ChannelAlias> Aliases { get; set; } = [];
|
||||
public long Checkpoint { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JsonElement> AdditionalData { get; set; } = new();
|
||||
|
||||
public override string EntityKey
|
||||
{
|
||||
get => $"Channel_{Id}";
|
||||
init { }
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelAlias
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public string? ChannelId { get; set; }
|
||||
[JsonIgnore]
|
||||
public Channel? Channel { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Laraue.EfCoreTriggers.SqlLite.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
@ -9,6 +10,7 @@ public class DesignTimeAppContextFactory : IDesignTimeDbContextFactory<AppDbCont
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
|
||||
optionsBuilder.UseSqlite("Data Source=fake.db");
|
||||
optionsBuilder.UseSqlLiteTriggers();
|
||||
|
||||
return new AppDbContext(optionsBuilder.Options);
|
||||
}
|
||||
|
||||
23
BTCPayApp.Core/Data/EFExtensions.cs
Normal file
23
BTCPayApp.Core/Data/EFExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class EFExtensions
|
||||
{
|
||||
|
||||
public static async Task<int> Upsert<T>(this DbContext ctx, T item, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
return await ctx.Upsert(item).RunAsync(cancellationToken);
|
||||
// ctx.Attach(item);
|
||||
// ctx.Entry(item).State = EntityState.Modified;
|
||||
// try
|
||||
// {
|
||||
// return await ctx.SaveChangesAsync(cancellationToken);
|
||||
// }
|
||||
// catch (DbUpdateException)
|
||||
// {
|
||||
// ctx.Entry(item).State = EntityState.Added;
|
||||
// return await ctx.SaveChangesAsync(cancellationToken);
|
||||
// }
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,13 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
public class LightningConfig
|
||||
{
|
||||
public const string Key = "lightningconfig";
|
||||
public const string Key = "ln:lightningconfig";
|
||||
|
||||
public string Alias { get; set; } = "BTCPay Server";
|
||||
public string ScriptDerivationKey { get; set; } = WalletDerivation.NativeSegwit; //when ldk asks for an address, where do we get it from?
|
||||
public string LightningDerivationPath { get; set; } = "m/666'";// your lightning node derivation path
|
||||
public string Color { get; set; } = "#51B13E";
|
||||
public Uri? RapidGossipSyncUrl { get; set; }
|
||||
|
||||
public string? JITLSP { get; set; } // Just In Time Lightning Service Provider
|
||||
|
||||
@ -17,7 +18,7 @@ public class LightningConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrEmpty(Color)){ return [0,0,0];}
|
||||
if (string.IsNullOrEmpty(Color)){ return [0,0,0];}
|
||||
|
||||
if (Color.StartsWith("#"))
|
||||
{
|
||||
@ -38,15 +39,8 @@ public class LightningConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Dictionary<string, PeerInfo> Peers { get; set; } = new();
|
||||
|
||||
public bool AcceptInboundConnection{ get; set; }
|
||||
}
|
||||
|
||||
public record PeerInfo
|
||||
{
|
||||
public string Endpoint { get; set; }
|
||||
public bool Persistent { get; set; }
|
||||
public bool Trusted { get; set; }
|
||||
}
|
||||
8
BTCPayApp.Core/Data/LogDbContext.cs
Normal file
8
BTCPayApp.Core/Data/LogDbContext.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogDbContext(DbContextOptions<LogDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<LogEntry> Logs { get; set; }
|
||||
}
|
||||
13
BTCPayApp.Core/Data/LogEntry.cs
Normal file
13
BTCPayApp.Core/Data/LogEntry.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class LogEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Level { get; set; }
|
||||
public DateTime TimeStamp { get; set; }
|
||||
public string RenderedMessage { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public string Properties { get; set; }
|
||||
}
|
||||
42
BTCPayApp.Core/Data/LoggingConfig.cs
Normal file
42
BTCPayApp.Core/Data/LoggingConfig.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class LoggingConfig
|
||||
{
|
||||
private const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}";
|
||||
|
||||
public static void ConfigureLogging(IServiceCollection serviceCollection)
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
var dirProvider = serviceProvider.GetRequiredService<IDataDirectoryProvider>();
|
||||
var appDir = dirProvider.GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var dbPath = $"{appDir}/logs.db";
|
||||
var isDevEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
|
||||
var minLogLevel = isDevEnv ? LogEventLevel.Verbose : LogEventLevel.Information;
|
||||
|
||||
serviceCollection.AddSerilog();
|
||||
serviceCollection.AddDbContextFactory<LogDbContext>((_, options) =>
|
||||
{
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
});
|
||||
|
||||
/*
|
||||
"LDK": "Trace",
|
||||
"LDK.lightning::ln::peer_handler": "Debug",
|
||||
"LDK.lightning::routing::gossip": "Information",
|
||||
"LDK.BTCPayApp.Core.LDK.LDKPeerHandler": "Information",
|
||||
"LDK.lightning_background_processor": "Information"*/
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SQLite(dbPath)
|
||||
.WriteTo.Console(outputTemplate: OutputTemplate, restrictedToMinimumLevel: minLogLevel)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning).CreateLogger();
|
||||
}
|
||||
}
|
||||
10
BTCPayApp.Core/Data/Outbox.cs
Normal file
10
BTCPayApp.Core/Data/Outbox.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Outbox
|
||||
{
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
|
||||
public OutboxAction ActionType { get; set; }
|
||||
public required string Key { get; set; }
|
||||
public required string Entity { get; set; }
|
||||
public required long Version { get; set; }
|
||||
}
|
||||
11
BTCPayApp.Core/Data/OutboxAction.cs
Normal file
11
BTCPayApp.Core/Data/OutboxAction.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OutboxAction
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
14
BTCPayApp.Core/Data/PeerInfo.cs
Normal file
14
BTCPayApp.Core/Data/PeerInfo.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public record PeerInfo
|
||||
{
|
||||
[JsonConverter(typeof(EndPointJsonConverter))]
|
||||
public EndPoint? Endpoint { get; set; }
|
||||
public bool Persistent { get; set; }
|
||||
public bool Trusted { get; set; }
|
||||
public string? Label { get; set; }
|
||||
}
|
||||
@ -2,9 +2,16 @@
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class Setting
|
||||
public class Setting:VersionedData
|
||||
{
|
||||
[Key]
|
||||
public string Key { get; set; }
|
||||
public byte[] Value { get; set; }
|
||||
}
|
||||
public required string Key { get; set; }
|
||||
public byte[]? Value { get; set; }
|
||||
public bool Backup { get; set; } = true;
|
||||
|
||||
public override string EntityKey
|
||||
{
|
||||
get => $"Setting_{Key}";
|
||||
init { }
|
||||
}
|
||||
}
|
||||
|
||||
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
43
BTCPayApp.Core/Data/TLVHelper.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class TLVHelper
|
||||
{
|
||||
public record TLV(byte Tag, byte[] Value);
|
||||
|
||||
public static byte[] Write(List<TLV> tlvList)
|
||||
{
|
||||
List<byte> byteArray = new List<byte>();
|
||||
|
||||
foreach (var tlv in tlvList)
|
||||
{
|
||||
byteArray.Add(tlv.Tag);
|
||||
byteArray.AddRange(BitConverter.GetBytes(tlv.Value.Length));
|
||||
byteArray.AddRange(tlv.Value);
|
||||
}
|
||||
|
||||
return byteArray.ToArray();
|
||||
}
|
||||
|
||||
public static List<TLV> Read(byte[] byteArray)
|
||||
{
|
||||
var tlvList = new List<TLV>();
|
||||
var index = 0;
|
||||
|
||||
while (index < byteArray.Length)
|
||||
{
|
||||
var tag = byteArray[index];
|
||||
index += 1;
|
||||
|
||||
var length = BitConverter.ToInt32(byteArray, index);
|
||||
index += 4;
|
||||
|
||||
var value = new byte[length];
|
||||
Array.Copy(byteArray, index, value, 0, length);
|
||||
index += length;
|
||||
|
||||
tlvList.Add(new TLV(tag, value));
|
||||
}
|
||||
|
||||
return tlvList;
|
||||
}
|
||||
}
|
||||
7
BTCPayApp.Core/Data/TriggerRecord.cs
Normal file
7
BTCPayApp.Core/Data/TriggerRecord.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
class TriggerRecord
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string sql { get; set; }
|
||||
}
|
||||
36
BTCPayApp.Core/Data/ValueConversionExtensions.cs
Normal file
36
BTCPayApp.Core/Data/ValueConversionExtensions.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public static class ValueConversionExtensions
|
||||
{
|
||||
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder)
|
||||
where T : class, new()
|
||||
{
|
||||
var converter = new ValueConverter<T, string>
|
||||
(
|
||||
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
|
||||
v => JsonSerializer.Deserialize<T>(v, JsonSerializerOptions.Default) ?? new T()
|
||||
);
|
||||
|
||||
var comparer = new ValueComparer<T>
|
||||
(
|
||||
(l, r) => JsonSerializer.Serialize(l, JsonSerializerOptions.Default) ==
|
||||
JsonSerializer.Serialize(r, JsonSerializerOptions.Default),
|
||||
v => v == null ? 0 : JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(),
|
||||
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
|
||||
JsonSerializerOptions.Default)!
|
||||
);
|
||||
|
||||
propertyBuilder.HasConversion(converter);
|
||||
propertyBuilder.Metadata.SetValueConverter(converter);
|
||||
propertyBuilder.Metadata.SetValueComparer(comparer);
|
||||
propertyBuilder.HasColumnType("jsonb");
|
||||
|
||||
return propertyBuilder;
|
||||
}
|
||||
}
|
||||
11
BTCPayApp.Core/Data/VersionedData.cs
Normal file
11
BTCPayApp.Core/Data/VersionedData.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public abstract class VersionedData
|
||||
{
|
||||
public long Version { get; set; } = 0;
|
||||
|
||||
public abstract string EntityKey { get; init; }
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
using NBitcoin;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayApp.Core.JsonConverters;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
@ -8,14 +10,40 @@ public class WalletConfig
|
||||
|
||||
public required string Mnemonic { get; set; }
|
||||
public required string Network { get; set; }
|
||||
|
||||
|
||||
//key is the identifier of the tracker, value is a sub wallet format.
|
||||
//key is the identifier of the tracker, value is a sub wallet format.
|
||||
//for example, we will track native segwit wallet, the descriptor will be wpkh([fingerprint/84'/0'/0']xpub/0/*)
|
||||
// or for LN specifics, the descriptor is null, and we track non deterministic scripts
|
||||
public Dictionary<string, WalletDerivation> Derivations { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public string Fingerprint => new Mnemonic(Mnemonic).DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString();
|
||||
[JsonIgnore]
|
||||
public Network? NBitcoinNetwork => NBitcoin.Network.GetNetwork(Network);
|
||||
|
||||
|
||||
}
|
||||
public required BlockSnapshot Birthday { get; set; }
|
||||
|
||||
public required CoinSnapshot CoinSnapshot { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class CoinSnapshot
|
||||
{
|
||||
public required BlockSnapshot BlockSnapshot { get; set; }
|
||||
public required Dictionary<string, SavedCoin[]> Coins { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class SavedCoin
|
||||
{
|
||||
[JsonConverter(typeof(BitcoinSerializableJsonConverterFactory))]
|
||||
public required OutPoint Outpoint { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
public KeyPath? Path { get; set; }
|
||||
}
|
||||
|
||||
public class BlockSnapshot
|
||||
{
|
||||
public required uint BlockHeight { get; set; }
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public required uint256 BlockHash { get; set; }
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
namespace BTCPayApp.Core.Data;
|
||||
namespace BTCPayApp.Core.Data;
|
||||
|
||||
public class WalletDerivation
|
||||
{
|
||||
public string Identifier { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string? Descriptor { get; set; }
|
||||
|
||||
public const string NativeSegwit = "segwit";
|
||||
public const string LightningScripts = "lightningScripts";
|
||||
// public const string SpendableOutputs = "spendableOutputs";
|
||||
}
|
||||
|
||||
public required string Name { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? Descriptor { get; set; }
|
||||
|
||||
// TODO: this is useful when restoring, to tell NBX to generate addresses up to this to prevent address reuse.
|
||||
public int? LastKnownIndex{ get; set; }
|
||||
}
|
||||
|
||||
@ -1,31 +1,116 @@
|
||||
using System.Text.Json;
|
||||
// using System.Collections.Concurrent;
|
||||
// using System.Text.Json;
|
||||
// using BTCPayApp.Core.Attempt2;
|
||||
// using BTCPayApp.Core.Contracts;
|
||||
// using BTCPayApp.Core.Data;
|
||||
// using BTCPayApp.VSS;
|
||||
// using BTCPayServer.Lightning;
|
||||
// using Microsoft.EntityFrameworkCore;
|
||||
// using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
// using Microsoft.Extensions.Logging;
|
||||
// using VSSProto;
|
||||
//
|
||||
// namespace BTCPayApp.Core;
|
||||
//
|
||||
//
|
||||
//
|
||||
// public class VSSMapperInterceptor : SaveChangesInterceptor
|
||||
// {
|
||||
//
|
||||
// public VSSMapperInterceptor(BTCPayConnectionManager btcPayConnectionManager, ILogger<VSSMapperInterceptor> logger)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// private ConcurrentDictionary<EventId, object> PendingEvents = new ConcurrentDictionary<EventId, object>();
|
||||
// public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
|
||||
// CancellationToken cancellationToken = new CancellationToken())
|
||||
// {
|
||||
// return base.SavedChangesAsync(eventData, result, cancellationToken);
|
||||
// }
|
||||
//
|
||||
// private IVSSAPI api;
|
||||
// public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
|
||||
// CancellationToken cancellationToken = new CancellationToken())
|
||||
// {
|
||||
// foreach (var entry in eventData.Context.ChangeTracker.Entries())
|
||||
// {
|
||||
//
|
||||
// if (entry.Entity is LightningPayment lightningPayment)
|
||||
// {
|
||||
// if (entry.State == EntityState.Deleted)
|
||||
// {
|
||||
//
|
||||
// api.DeleteObjectAsync(new DeleteObjectRequest
|
||||
// {
|
||||
// KeyValue = new KeyValue()
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// Key = $"LightningPayment/{lightningPayment.Id}"
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// if (entry.Entity is Channel channel)
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// if (entry.Entity is Setting setting)
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
// }
|
||||
//
|
||||
// public override Task SaveChangesCanceledAsync(DbContextEventData eventData,
|
||||
// CancellationToken cancellationToken = new CancellationToken())
|
||||
// {
|
||||
// PendingEvents.Remove(eventData.EventId, out _);
|
||||
// return base.SaveChangesCanceledAsync(eventData, cancellationToken);
|
||||
// }
|
||||
//
|
||||
// public override Task SaveChangesFailedAsync(DbContextErrorEventData eventData,
|
||||
// CancellationToken cancellationToken = new CancellationToken())
|
||||
// {
|
||||
// PendingEvents.Remove(eventData.EventId, out _);
|
||||
// return base.SaveChangesFailedAsync(eventData, cancellationToken);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
|
||||
using System.Text.Json;
|
||||
using AsyncKeyedLock;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
|
||||
public class DatabaseConfigProvider: IConfigProvider
|
||||
public class DatabaseConfigProvider(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
ILogger<DatabaseConfigProvider> logger)
|
||||
: ConfigProvider
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly AsyncKeyedLocker<string> _lock = new();
|
||||
|
||||
public DatabaseConfigProvider(IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
public override async Task<T?> Get<T>(string key) where T : default
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<T?> Get<T>(string key)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
var config = await dbContext.Settings.FindAsync(key);
|
||||
if (typeof(T) == typeof(byte[]))
|
||||
return (T?) (config?.Value as object);
|
||||
return config is null ? default : JsonSerializer.Deserialize<T>(config.Value);
|
||||
}
|
||||
|
||||
public async Task Set<T>(string key, T? value)
|
||||
public override async Task Set<T>(string key, T? value, bool backup) where T : default
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
using var releaser = await _lock.LockAsync(key);
|
||||
logger.LogDebug("Setting {Key} to {Value} {Backup}", key, value, backup ? "backup": "no backup");
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
if (value is null)
|
||||
{
|
||||
try
|
||||
@ -40,13 +125,13 @@ public class DatabaseConfigProvider: IConfigProvider
|
||||
}
|
||||
|
||||
var newValue = typeof(T) == typeof(byte[])? value as byte[]:JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
await dbContext.Upsert(new Setting {Key = key, Value = newValue}).RunAsync();
|
||||
|
||||
var setting = new Setting {Key = key, Value = newValue, Backup = backup};
|
||||
await dbContext.Upsert(setting, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> List(string prefix)
|
||||
public override async Task<IEnumerable<string>> List(string prefix)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Settings.Where(s => s.Key.StartsWith(prefix)).Select(s => s.Key).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
76
BTCPayApp.Core/Extensions/DebugExtensions.cs
Normal file
76
BTCPayApp.Core/Extensions/DebugExtensions.cs
Normal file
@ -0,0 +1,76 @@
|
||||
#if DEBUG
|
||||
|
||||
using System.Net.Security;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace BTCPayApp.Core.Extensions;
|
||||
|
||||
public class DangerousHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public static bool ServerValidate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
|
||||
{
|
||||
if (errors == SslPolicyErrors.None) return true;
|
||||
return certificate?.Subject.Equals("CN=localhost") is true;
|
||||
}
|
||||
|
||||
private static HttpClientHandler GetInsecureHandler()
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
handler.ServerCertificateCustomValidationCallback = ServerValidate;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient(GetInsecureHandler());
|
||||
}
|
||||
}
|
||||
|
||||
#if ANDROID
|
||||
public class DangerousAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
|
||||
{
|
||||
protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
|
||||
=> new CustomHostnameVerifier();
|
||||
|
||||
private sealed class CustomHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier
|
||||
{
|
||||
public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
|
||||
{
|
||||
return session?.PeerPrincipal?.Name == "CN=localhost";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public static class DebugExtensions
|
||||
{
|
||||
public static IServiceCollection AddDangerousSSLSettingsForDev(this IServiceCollection services)
|
||||
{
|
||||
services.Replace(ServiceDescriptor.Singleton<IHttpClientFactory, DangerousHttpClientFactory>());
|
||||
|
||||
services.AddSingleton<Func<HttpMessageHandler, HttpMessageHandler>>(handler =>
|
||||
{
|
||||
if (handler is HttpClientHandler clientHandler)
|
||||
{
|
||||
// always verify the SSL certificate
|
||||
clientHandler.ServerCertificateCustomValidationCallback += DangerousHttpClientFactory.ServerValidate;
|
||||
return clientHandler;
|
||||
}
|
||||
#if ANDROID
|
||||
return new DangerousAndroidMessageHandler();
|
||||
#else
|
||||
return handler;
|
||||
#endif
|
||||
});
|
||||
|
||||
services.AddSingleton<Action<ClientWebSocketOptions>>(provider => wsc =>
|
||||
{
|
||||
wsc.RemoteCertificateValidationCallback = DangerousHttpClientFactory.ServerValidate;
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,16 +1,20 @@
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayApp.Core.Attempt2;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Backup;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Services;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using Laraue.EfCoreTriggers.SqlLite.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core;
|
||||
namespace BTCPayApp.Core.Extensions;
|
||||
|
||||
public static class StartupExtensions
|
||||
{
|
||||
@ -20,10 +24,19 @@ public static class StartupExtensions
|
||||
{
|
||||
var dir = provider.GetRequiredService<IDataDirectoryProvider>().GetAppDataDirectory().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
options.UseSqlite($"Data Source={dir}/app.db");
|
||||
options.UseSqlLiteTriggers();
|
||||
});
|
||||
|
||||
// Configure logging
|
||||
LoggingConfig.ConfigureLogging(serviceCollection);
|
||||
|
||||
serviceCollection.AddHostedService<AppDatabaseMigrator>();
|
||||
serviceCollection.AddSingleton<ConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddMemoryCache();
|
||||
serviceCollection.AddHttpClient();
|
||||
serviceCollection.AddSingleton<BTCPayConnectionManager>();
|
||||
serviceCollection.AddSingleton<SyncService>();
|
||||
serviceCollection.AddSingleton<LoggingService>();
|
||||
serviceCollection.AddSingleton<LightningNodeManager>();
|
||||
serviceCollection.AddSingleton<OnChainWalletManager>();
|
||||
serviceCollection.AddSingleton<BTCPayAppServerClient>();
|
||||
@ -32,38 +45,13 @@ public static class StartupExtensions
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<LightningNodeManager>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<OnChainWalletManager>());
|
||||
serviceCollection.AddSingleton<AuthStateProvider>();
|
||||
serviceCollection.AddSingleton<AuthenticationStateProvider, AuthStateProvider>( provider => provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton<AuthenticationStateProvider, AuthStateProvider>(provider => provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton<IHostedService>(provider => provider.GetRequiredService<AuthStateProvider>());
|
||||
serviceCollection.AddSingleton(sp => (IAccountManager)sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
serviceCollection.AddSingleton<IConfigProvider, DatabaseConfigProvider>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, AuthorizationHandler>();
|
||||
serviceCollection.AddAuthorizationCore(options => options.AddPolicies());
|
||||
serviceCollection.AddLDK();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppDatabaseMigrator: IHostedService
|
||||
{
|
||||
private readonly ILogger<AppDatabaseMigrator> _logger;
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
|
||||
public AppDatabaseMigrator(ILogger<AppDatabaseMigrator> logger, IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var pendingMigrationsAsync = (await dbContext.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken)).ToArray();
|
||||
if (pendingMigrationsAsync.Any())
|
||||
{
|
||||
_logger.LogInformation($"Applying {pendingMigrationsAsync.Length} migrations");
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
_logger.LogInformation("Migrations applied: " + string.Join(", ", pendingMigrationsAsync));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken) { }
|
||||
}
|
||||
@ -6,16 +6,24 @@ namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class AsyncExtensions
|
||||
{
|
||||
|
||||
public static async Task RunSync(this Task task)
|
||||
public static async Task RunInOtherThread(Action action)
|
||||
{
|
||||
|
||||
task.GetAwaiter().GetResult();
|
||||
await Task.Factory.StartNew(action);
|
||||
}
|
||||
|
||||
public static async Task<T> RunSync<T>(this Task<T> task)
|
||||
public static async Task<T> RunInOtherThread<T>(Func<T> action)
|
||||
{
|
||||
return task.GetAwaiter().GetResult();
|
||||
return await Task.Factory.StartNew(action);
|
||||
}
|
||||
|
||||
public static async Task RunInOtherThread(this Task task)
|
||||
{
|
||||
await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
}
|
||||
|
||||
public static async Task<T> RunInOtherThread<T>(this Task<T> task)
|
||||
{
|
||||
return await Task.Factory.StartNew(async () => await task).Unwrap();
|
||||
}
|
||||
/// <summary>
|
||||
/// Allows a cancellation token to be awaited.
|
||||
@ -46,7 +54,7 @@ public static class AsyncExtensions
|
||||
public object GetResult()
|
||||
{
|
||||
// this is called by compiler generated methods when the
|
||||
// task has completed. Instead of returning a result, we
|
||||
// task has completed. Instead of returning a result, we
|
||||
// just throw an exception.
|
||||
if (IsCompleted) throw new OperationCanceledException();
|
||||
else throw new InvalidOperationException("The cancellation token has not yet been cancelled.");
|
||||
@ -64,4 +72,4 @@ public static class AsyncExtensions
|
||||
public void UnsafeOnCompleted(Action continuation) =>
|
||||
CancellationToken.Register(continuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
BTCPayApp.Core/Helpers/AuthorizationOptionsExtensions.cs
Normal file
36
BTCPayApp.Core/Helpers/AuthorizationOptionsExtensions.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
// Copied from BTCPayServer
|
||||
public static class AuthorizationOptionsExtensions
|
||||
{
|
||||
public static AuthorizationOptions AddPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
// BTCPay policies
|
||||
foreach (var p in Policies.AllPolicies)
|
||||
{
|
||||
options.AddPolicy(p);
|
||||
}
|
||||
options.AddPolicy(Policies.CanModifyStoreSettingsUnscoped);
|
||||
options.AddPolicy(CanGetRates.Key);
|
||||
// app policies
|
||||
foreach (var p in AppPolicies.AllPolicies)
|
||||
{
|
||||
options.AddPolicy(p);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void AddPolicy(this AuthorizationOptions options, string policy)
|
||||
{
|
||||
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
|
||||
}
|
||||
|
||||
private class CanGetRates
|
||||
{
|
||||
public const string Key = "btcpay.store.cangetrates";
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,42 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public abstract class BaseHostedService : IHostedService, IDisposable
|
||||
public abstract class BaseHostedService(ILogger logger) : IHostedService, IDisposable
|
||||
{
|
||||
protected CancellationTokenSource _cancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim _controlSemaphore = new(1, 1);
|
||||
private Task? _currentTask;
|
||||
protected CancellationTokenSource CancellationTokenSource = new();
|
||||
protected readonly SemaphoreSlim ControlSemaphore = new(1, 1);
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _controlSemaphore.WaitAsync(cancellationToken);
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
await ExecuteStartAsync(CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken).Token);
|
||||
CancellationTokenSource = new CancellationTokenSource();
|
||||
await ExecuteStartAsync(CancellationTokenSource.CreateLinkedTokenSource(CancellationTokenSource.Token, cancellationToken).Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
ControlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
await _controlSemaphore.WaitAsync(cancellationToken);
|
||||
logger.LogInformation("Stopping service");
|
||||
await CancellationTokenSource.CancelAsync();
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteStopAsync(_cancellationTokenSource.Token);
|
||||
await ExecuteStopAsync(CancellationTokenSource.Token);
|
||||
|
||||
logger.LogInformation("Stopped");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_controlSemaphore.Release();
|
||||
ControlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +45,20 @@ public abstract class BaseHostedService : IHostedService, IDisposable
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_controlSemaphore?.Dispose();
|
||||
CancellationTokenSource?.Dispose();
|
||||
ControlSemaphore?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task WrapInLock(Func<Task> act, CancellationToken cancellationToken)
|
||||
{
|
||||
await ControlSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ControlSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,10 +9,10 @@ namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ChannelExtensions
|
||||
{
|
||||
|
||||
|
||||
public static IDisposable SubscribeToEventWithChannelQueue<TEvent>(
|
||||
Action<AsyncEventHandler<TEvent>> add,
|
||||
Action<AsyncEventHandler<TEvent>> remove,
|
||||
Action<AsyncEventHandler<TEvent>> remove,
|
||||
Func<TEvent, CancellationToken, Task> processor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@ -23,47 +23,45 @@ public static class ChannelExtensions
|
||||
await channel.Writer.WriteAsync(evt, cancellationToken);
|
||||
}
|
||||
|
||||
add(new AsyncEventHandler<TEvent>(OnEvent));
|
||||
_ = ProcessChannel(channel, processor, cancellationToken);
|
||||
add(OnEvent);
|
||||
_ = channel.ProcessChannel(processor, cancellationToken);
|
||||
|
||||
return new DisposableWrapper(async () =>
|
||||
return new DisposableWrapper(() =>
|
||||
{
|
||||
remove(new AsyncEventHandler<TEvent>(OnEvent));
|
||||
remove(OnEvent);
|
||||
channel.Writer.Complete();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task ProcessChannel<TEvent>(Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
|
||||
private static async Task ProcessChannel<TEvent>(this Channel<TEvent> channel, Func<TEvent, CancellationToken, Task> processor, CancellationToken cancellationToken)
|
||||
{
|
||||
while (await channel.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
while (channel.Reader.TryRead(out TEvent item))
|
||||
while (channel.Reader.TryRead(out var item))
|
||||
{
|
||||
await processor(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static (BitcoinExtPubKey, RootedKeyPath, ScriptPubKeyType)? ExtractFromDescriptor(this string descriptor, Network network)
|
||||
public static (BitcoinExtPubKey, RootedKeyPath?, ScriptPubKeyType)? ExtractFromDescriptor(this string descriptor, Network network)
|
||||
{
|
||||
var od = OutputDescriptor.Parse(descriptor, network);
|
||||
|
||||
(BitcoinExtPubKey, RootedKeyPath) ExtractFromPkProvider(PubKeyProvider pubKeyProvider)
|
||||
(BitcoinExtPubKey, RootedKeyPath?) ExtractFromPkProvider(PubKeyProvider pubKeyProvider)
|
||||
{
|
||||
switch (pubKeyProvider)
|
||||
{
|
||||
case PubKeyProvider.Const _:
|
||||
throw new FormatException("Only HD output descriptors are supported.");
|
||||
case PubKeyProvider.HD hd:
|
||||
if (hd.Path != null && hd.Path.ToString() != "0")
|
||||
{
|
||||
if (hd.Path is not null && hd.Path.ToString() != "0")
|
||||
throw new FormatException("Custom change paths are not supported.");
|
||||
}
|
||||
return (hd.Extkey, null);
|
||||
case PubKeyProvider.Origin origin:
|
||||
var innerResult = ExtractFromPkProvider(origin.Inner);
|
||||
return (innerResult.Item1, origin.KeyOriginInfo );
|
||||
return (innerResult.Item1, origin.KeyOriginInfo);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
@ -81,20 +79,25 @@ public static class ChannelExtensions
|
||||
public static UserConfig AsLDKUserConfig(this LightningConfig config)
|
||||
{
|
||||
var result = UserConfig.with_default();
|
||||
// var channelConfig = ChannelConfig.with_default();
|
||||
// channelConfig.set
|
||||
// result.set_channel_config(channelConfig);
|
||||
result.set_accept_intercept_htlcs(true);
|
||||
result.set_accept_mpp_keysend(true);
|
||||
//result.set_accept_mpp_keysend(true);
|
||||
result.set_manually_accept_inbound_channels(true);
|
||||
var channelHandshakeConfig = ChannelHandshakeConfig.with_default();
|
||||
channelHandshakeConfig.set_announced_channel(false);
|
||||
//channelHandshakeConfig.set_announced_channel(false);
|
||||
// channelHandshakeConfig.set(false);
|
||||
channelHandshakeConfig.set_negotiate_anchors_zero_fee_htlc_tx(true);
|
||||
channelHandshakeConfig.set_minimum_depth(1);
|
||||
result.set_channel_handshake_config(channelHandshakeConfig);
|
||||
var channelHandshakeLimits = ChannelHandshakeLimits.with_default();
|
||||
channelHandshakeLimits.set_force_announced_channel_preference(true);
|
||||
channelHandshakeLimits.set_max_funding_satoshis(Money.Coins(100m).Satoshi);
|
||||
result.set_channel_handshake_limits(channelHandshakeLimits);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// public static async Task Process<T>(this Channel<T> channel, Func<T, CancellationToken, Task> processor,
|
||||
// CancellationToken cancellationToken)
|
||||
// {
|
||||
@ -172,4 +175,4 @@ public static class ChannelExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +31,16 @@ public static class ChannelManagerHelper
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
public static ChannelManager? Load(ChannelMonitor[] channelMonitors, byte[] channelManagerSerialized,
|
||||
EntropySource entropySource, SignerProvider signerProvider,
|
||||
NodeSigner nodeSigner, FeeEstimator feeEstimator,
|
||||
Watch watch, BroadcasterInterface txBroadcaster,
|
||||
Router router, Logger logger, UserConfig config, Filter filter)
|
||||
Router router, MessageRouter messageRouter, Logger logger, UserConfig config, Filter filter)
|
||||
{
|
||||
var resManager = UtilMethods.C2Tuple_ThirtyTwoBytesChannelManagerZ_read(channelManagerSerialized, entropySource,
|
||||
nodeSigner, signerProvider, feeEstimator,
|
||||
watch, txBroadcaster,
|
||||
router, logger, config, channelMonitors);
|
||||
router, messageRouter, logger, config, channelMonitors);
|
||||
if (!resManager.is_ok())
|
||||
{
|
||||
throw new SerializationException("Serialized ChannelManager was corrupt");
|
||||
@ -55,4 +54,4 @@ public static class ChannelManagerHelper
|
||||
return (resManager as Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ.
|
||||
Result_C2Tuple_ThirtyTwoBytesChannelManagerZDecodeErrorZ_OK)?.res.get_b();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
BTCPayApp.Core/Helpers/ConfigExtensions.cs
Normal file
27
BTCPayApp.Core/Helpers/ConfigExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ConfigExtensions
|
||||
{
|
||||
private const string ConfigDeviceIdentifierKey = "deviceIdentifier";
|
||||
|
||||
/*
|
||||
public static async Task<long> GetDeviceIdentifier(this ISecureConfigProvider configProvider)
|
||||
{
|
||||
var id = await configProvider.Get<long>(ConfigDeviceIdentifierKey);
|
||||
if (id == 0)
|
||||
{
|
||||
id = RandomUtils.GetInt64();
|
||||
await configProvider.Set(ConfigDeviceIdentifierKey, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
*/
|
||||
|
||||
public static async Task<long> GetDeviceIdentifier(this ConfigProvider configProvider)
|
||||
{
|
||||
return await configProvider.GetOrSet(ConfigDeviceIdentifierKey, () => Task.FromResult(RandomUtils.GetInt64()), false);
|
||||
}
|
||||
}
|
||||
24
BTCPayApp.Core/Helpers/ConfigHelpers.cs
Normal file
24
BTCPayApp.Core/Helpers/ConfigHelpers.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class ConfigHelpers
|
||||
{
|
||||
public static async Task<T?> GetOrSet<T>(this ISecureConfigProvider secureConfigProvider, string key, Func<Task<T>> factory)
|
||||
{
|
||||
var value = await secureConfigProvider.Get<T>(key);
|
||||
if (!Equals(value, default(T))) return value;
|
||||
value = await factory();
|
||||
await secureConfigProvider.Set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public static async Task<T?> GetOrSet<T>(this ConfigProvider configProvider, string key, Func<Task<T>> factory, bool backup)
|
||||
{
|
||||
var value = await configProvider.Get<T>(key);
|
||||
if (!Equals(value, default(T))) return value;
|
||||
value = await factory();
|
||||
await configProvider.Set(key, value, backup);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -2,30 +2,27 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
//from wasabi
|
||||
//from wasabi
|
||||
public static class EndPointParser
|
||||
{
|
||||
|
||||
public static IPEndPoint IPEndPoint(this EndPoint endPoint)
|
||||
{
|
||||
|
||||
if(endPoint is IPEndPoint ipEndPoint)
|
||||
return ipEndPoint;
|
||||
if(endPoint is not DnsEndPoint dnsEndPoint)
|
||||
throw new FormatException($"Invalid endpoint: {endPoint}");
|
||||
|
||||
|
||||
var addresses = System.Net.Dns.GetHostAddresses(dnsEndPoint.Host);
|
||||
|
||||
var addresses = Dns.GetHostAddresses(dnsEndPoint.Host);
|
||||
if (addresses.Length == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Unable to retrieve address from specified host name.",
|
||||
"Unable to retrieve address from specified host name.",
|
||||
"hostName"
|
||||
);
|
||||
}
|
||||
return new IPEndPoint(addresses[0], dnsEndPoint.Port); // Port gets validated here.
|
||||
}
|
||||
|
||||
|
||||
public static string Host(this EndPoint me)
|
||||
{
|
||||
if (me is DnsEndPoint dnsEndPoint)
|
||||
@ -41,7 +38,7 @@ public static class EndPointParser
|
||||
throw new FormatException($"Invalid endpoint: {me}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int? Port(this EndPoint me)
|
||||
{
|
||||
var result = 0;
|
||||
@ -63,7 +60,7 @@ public static class EndPointParser
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static string ToString(this EndPoint me, int defaultPort)
|
||||
{
|
||||
string host = me.Host();
|
||||
@ -73,11 +70,9 @@ public static class EndPointParser
|
||||
return endPointString;
|
||||
}
|
||||
|
||||
/// <param name="defaultPort">If invalid and it's needed to use, then this function returns false.</param>
|
||||
public static bool TryParse(string? endPointString, int defaultPort, [NotNullWhen(true)] out EndPoint? endPoint)
|
||||
{
|
||||
|
||||
if(System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
if (!string.IsNullOrEmpty(endPointString) && System.Net.IPEndPoint.TryParse(endPointString, out var ipEndPoint))
|
||||
{
|
||||
if (ipEndPoint.Port == 0)
|
||||
{
|
||||
@ -86,15 +81,12 @@ public static class EndPointParser
|
||||
endPoint = ipEndPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
endPoint = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endPointString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(endPointString)) return false;
|
||||
|
||||
endPointString = endPointString.TrimEnd(':', '/');
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public delegate Task AsyncEventHandler<TEventArgs>(object? sender, TEventArgs e);
|
||||
public delegate Task AsyncEventHandler<in TEventArgs>(object? sender, TEventArgs e);
|
||||
public delegate Task AsyncEventHandler(object? sender);
|
||||
|
||||
public static class EventHandlers
|
||||
@ -24,12 +24,12 @@ public static class EventHandlers
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
public static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
private static EventHandler<TArgs> TryAsync<TArgs>(
|
||||
this Func<object, TArgs, Task> callback,
|
||||
Func<Exception, Task> errorHandler)
|
||||
where TArgs : EventArgs
|
||||
{
|
||||
return new EventHandler<TArgs>(async (object s, TArgs e) =>
|
||||
return new EventHandler<TArgs>(async void (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -41,4 +41,4 @@ public static class EventHandlers
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ namespace BTCPayApp.Core.Helpers;
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged where TKey : notnull
|
||||
{
|
||||
private const string IndexerName = "Item[]";
|
||||
private const string? IndexerName = "Item[]";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> class that is empty, has the
|
||||
@ -109,10 +109,10 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
}
|
||||
|
||||
/// <summary>Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.</summary>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
|
||||
/// <summary>Occurs when a property value changes.</summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Uses the specified functions to add a key/value pair to the <see cref="ObservableConcurrentDictionary{TKey, TValue}"/> if the
|
||||
@ -275,7 +275,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
/// the default value of the <typeparamref name="TValue"/> type if <paramref name="key"/> does not exist.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if the object was removed successfully; otherwise, <see langword="false"/>.</returns>
|
||||
public new bool TryRemove(TKey key, out TValue value)
|
||||
public new bool TryRemove(TKey key, out TValue? value)
|
||||
{
|
||||
if (base.TryRemove(key, out value))
|
||||
{
|
||||
@ -319,5 +319,5 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : ConcurrentDic
|
||||
|
||||
/// <summary>Raises the <see cref="PropertyChanged" /> event.</summary>
|
||||
/// <param name="propertyName">Name of the property that has changed.</param>
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
14
BTCPayApp.Core/Helpers/PolicyRequirement.cs
Normal file
14
BTCPayApp.Core/Helpers/PolicyRequirement.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
// Copied from BTCPayServer
|
||||
public class PolicyRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public PolicyRequirement(string policy)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(policy);
|
||||
Policy = policy;
|
||||
}
|
||||
public string Policy { get; }
|
||||
}
|
||||
99
BTCPayApp.Core/Helpers/StoreHelpers.cs
Normal file
99
BTCPayApp.Core/Helpers/StoreHelpers.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.Core.LDK;
|
||||
using BTCPayApp.Core.Wallet;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers;
|
||||
|
||||
public static class StoreHelpers
|
||||
{
|
||||
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)>
|
||||
GetCurrentStorePaymentMethods(this IAccountManager accountManager)
|
||||
{
|
||||
var storeId = accountManager.CurrentStore?.Id;
|
||||
var pms = await accountManager.GetClient().GetStorePaymentMethods(storeId, includeConfig: true);
|
||||
var onchain = pms.FirstOrDefault(pm => pm.PaymentMethodId == OnChainWalletManager.PaymentMethodId);
|
||||
var lightning = pms.FirstOrDefault(pm => pm.PaymentMethodId == LightningNodeManager.PaymentMethodId);
|
||||
return (onchain, lightning);
|
||||
}
|
||||
|
||||
public static async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)?> TryApplyingAppPaymentMethodsToCurrentStore(
|
||||
this IAccountManager accountManager,
|
||||
OnChainWalletManager onChainWalletManager, LightningNodeManager lightningNodeService, bool applyOnchain, bool applyLighting)
|
||||
{
|
||||
var storeId = accountManager.CurrentStore?.Id;
|
||||
var userId = accountManager.UserInfo?.UserId;
|
||||
var config = await onChainWalletManager.GetConfig();
|
||||
if (// are user and store present?
|
||||
string.IsNullOrEmpty(userId) ||
|
||||
string.IsNullOrEmpty(storeId) ||
|
||||
// is user permitted? (store owner)
|
||||
!await accountManager.IsAuthorized(Policies.CanModifyStoreSettings, storeId) ||
|
||||
// is the onchain wallet configured?
|
||||
!OnChainWalletManager.IsConfigured(config)) return null;
|
||||
// check the store's payment methods
|
||||
var (onchain, lightning) = await GetCurrentStorePaymentMethods(accountManager);
|
||||
|
||||
// onchain
|
||||
if (applyOnchain && config?.Derivations.TryGetValue(WalletDerivation.NativeSegwit, out var derivation) is true && onchain is null)
|
||||
{
|
||||
onchain = await accountManager.GetClient().UpdateStorePaymentMethod(storeId, OnChainWalletManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = derivation.Descriptor
|
||||
});
|
||||
}
|
||||
|
||||
// lightning
|
||||
if (applyLighting && lightning is null && lightningNodeService is { IsActive: true, Node.ApiKeyManager: { } apiKeyManager })
|
||||
{
|
||||
var key = await apiKeyManager.GetKeyForStore(storeId, APIKeyPermission.Write);
|
||||
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId,
|
||||
LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = key.ConnectionString(userId)
|
||||
});
|
||||
}
|
||||
|
||||
return (onchain, lightning);
|
||||
}
|
||||
|
||||
public static async Task<bool> IsOnChainOurs(this OnChainWalletManager onChainWalletManager, GenericPaymentMethodData? onchain)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(onchain?.Config.ToString()))
|
||||
{
|
||||
var config = await onChainWalletManager.GetConfig();
|
||||
using var jsonDoc = JsonDocument.Parse(onchain.Config.ToString());
|
||||
if (jsonDoc.RootElement.TryGetProperty("accountDerivation", out var derivationSchemeElement) &&
|
||||
derivationSchemeElement.GetString() is { } derivationScheme &&
|
||||
config?.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}") is true)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<bool> IsLightningOurs(this LightningNodeManager lightningNodeManager, GenericPaymentMethodData? lightning)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(lightning?.Config.ToString()))
|
||||
{
|
||||
var node = lightningNodeManager.Node;
|
||||
var apiKeyManager = node?.ApiKeyManager;
|
||||
if (apiKeyManager == null) return false;
|
||||
using var jsonDoc = JsonDocument.Parse(lightning.Config.ToString());
|
||||
if (jsonDoc.RootElement.TryGetProperty("connectionString", out var connectionStringElement) &&
|
||||
connectionStringElement.GetString() is { } connectionString &&
|
||||
LightningConnectionStringHelper.ExtractValues(connectionString, out var lnConnectionString) is { } lnValues &&
|
||||
lnConnectionString == "app" && lnValues.TryGetValue("key", out var key) && key is not null &&
|
||||
await node!.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
81
BTCPayApp.Core/Helpers/SystemTextJsonMergeExtensions.cs
Normal file
81
BTCPayApp.Core/Helpers/SystemTextJsonMergeExtensions.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace BTCPayApp.Core.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// from https://github.com/dotnet/runtime/issues/31433#issuecomment-2148885279
|
||||
/// </summary>
|
||||
public static class SystemTextJsonMergeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges the specified Json Node into the base JsonNode for which this method is called.
|
||||
/// It is null safe and can be easily used with null-check & null coalesce operators for fluent calls.
|
||||
/// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects
|
||||
/// specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all
|
||||
/// fields so that they can be added to the base.
|
||||
///
|
||||
/// Source taken directly from the open-source Gist here:
|
||||
/// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="jsonBase"></param>
|
||||
/// <param name="jsonMerge"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static JsonNode Merge(this JsonNode jsonBase, JsonNode? jsonMerge)
|
||||
{
|
||||
if (jsonMerge == null)
|
||||
return jsonBase;
|
||||
|
||||
switch (jsonBase)
|
||||
{
|
||||
case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj:
|
||||
{
|
||||
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be
|
||||
// re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach...
|
||||
var mergeNodesArray = jsonMergeObj.ToArray();
|
||||
jsonMergeObj.Clear();
|
||||
|
||||
foreach (var prop in mergeNodesArray)
|
||||
{
|
||||
jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch
|
||||
{
|
||||
JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge(jsonMergeChildObj),
|
||||
JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge(jsonMergeChildArray),
|
||||
_ => prop.Value
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray:
|
||||
{
|
||||
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array,
|
||||
// so they can then be re-assigned to the target/base Json...
|
||||
var mergeNodesArray = jsonMergeArray.ToArray();
|
||||
jsonMergeArray.Clear();
|
||||
foreach(var mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " +
|
||||
$"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same.");
|
||||
|
||||
}
|
||||
|
||||
return jsonBase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the specified Dictionary of values into the base JsonNode for which this method is called.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="jsonBase"></param>
|
||||
/// <param name="dictionary"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static JsonNode MergeDictionary<TKey, TValue>(this JsonNode jsonBase, IDictionary<TKey, TValue> dictionary, JsonSerializerOptions? options = null)
|
||||
=> jsonBase.Merge(JsonSerializer.SerializeToNode(dictionary, options));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BOLT11PaymentRequestJsonConverter : GenericStringJsonConverter<BOLT11PaymentRequest>
|
||||
{
|
||||
public override BOLT11PaymentRequest Create(string str)
|
||||
{
|
||||
return NetworkHelper.Try(network => BOLT11PaymentRequest.Parse(str, network));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BitcoinSerializableJsonConverter<T> : GenericStringJsonConverter<T> where T : IBitcoinSerializable
|
||||
{
|
||||
public override T Create(string str)
|
||||
{
|
||||
var bytes = Convert.FromHexString(str);
|
||||
var instance = Activator.CreateInstance<T>();
|
||||
return NetworkHelper.Try(network =>
|
||||
{
|
||||
instance.ReadWrite(bytes, network);
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public override string? ToString(T? instance)
|
||||
{
|
||||
return Convert.ToHexString(instance.ToBytes()).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class BitcoinSerializableJsonConverterFactory : JsonConverterFactory
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeof(IBitcoinSerializable).IsAssignableFrom(typeToConvert);
|
||||
}
|
||||
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var converterType = typeof(BitcoinSerializableJsonConverter<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter) Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
}
|
||||
27
BTCPayApp.Core/JsonConverters/DateTimeToUnixTimeConverter.cs
Normal file
27
BTCPayApp.Core/JsonConverters/DateTimeToUnixTimeConverter.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class DateTimeToUnixTimeConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Null:
|
||||
return default;
|
||||
case JsonTokenType.Number:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
|
||||
case JsonTokenType.String:
|
||||
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(reader.GetString()!));
|
||||
}
|
||||
|
||||
throw new JsonException("Expected number or string with a unix timestamp value");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value.ToUnixTimeSeconds());
|
||||
}
|
||||
}
|
||||
37
BTCPayApp.Core/JsonConverters/GenericStringJsonConverter.cs
Normal file
37
BTCPayApp.Core/JsonConverters/GenericStringJsonConverter.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public abstract class GenericStringJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
public abstract T Create(string str);
|
||||
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null) return default;
|
||||
|
||||
if (reader.TokenType != JsonTokenType.String ||
|
||||
reader.GetString() is not { } str ||
|
||||
string.IsNullOrEmpty(str))
|
||||
throw new JsonException("Expected string");
|
||||
|
||||
return Create(str);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStringValue(ToString(value));
|
||||
}
|
||||
|
||||
public virtual string? ToString(T? value)
|
||||
{
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
11
BTCPayApp.Core/JsonConverters/KeyPathJsonConverter.cs
Normal file
11
BTCPayApp.Core/JsonConverters/KeyPathJsonConverter.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class KeyPathJsonConverter : GenericStringJsonConverter<KeyPath>
|
||||
{
|
||||
public override KeyPath Create(string str)
|
||||
{
|
||||
return new KeyPath(str);
|
||||
}
|
||||
}
|
||||
11
BTCPayApp.Core/JsonConverters/LightMoneyJsonConverter.cs
Normal file
11
BTCPayApp.Core/JsonConverters/LightMoneyJsonConverter.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public class LightMoneyJsonConverter : GenericStringJsonConverter<LightMoney>
|
||||
{
|
||||
public override LightMoney Create(string str)
|
||||
{
|
||||
return LightMoney.Parse(str);
|
||||
}
|
||||
}
|
||||
22
BTCPayApp.Core/JsonConverters/NetworkHelper.cs
Normal file
22
BTCPayApp.Core/JsonConverters/NetworkHelper.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayApp.Core.JsonConverters;
|
||||
|
||||
public static class NetworkHelper
|
||||
{
|
||||
public static T Try<T>(Func<Network, T> func)
|
||||
{
|
||||
Exception? lastException = null;
|
||||
foreach (var network in Network.GetNetworks())
|
||||
try
|
||||
{
|
||||
return func.Invoke(network);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
lastException = e;
|
||||
}
|
||||
|
||||
throw lastException!;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user