Compare commits
957 Commits
v1.0.3.1
...
v1.0.3.101
Author | SHA1 | Date | |
---|---|---|---|
560dde3396 | |||
7f9c2439c4 | |||
6de5d0bce8 | |||
c705a11aa7 | |||
45a196b407 | |||
07cb6adb69 | |||
5358f81ce0 | |||
5b7988be79 | |||
e6c794d68f | |||
de73fedd1b | |||
2719849a54 | |||
3011fecf0f | |||
6da0a9a201 | |||
572fe3eacb | |||
ff82f15246 | |||
b214e3f6df | |||
cb9130fdf9 | |||
925dc869a2 | |||
5f1aa619cd | |||
541c748ecb | |||
e853bddbc8 | |||
79d26b5d95 | |||
840f52a75b | |||
f955302c74 | |||
95e7d3dfc4 | |||
75f2749b19 | |||
01e5b319d1 | |||
e504163bc7 | |||
aba3f7d6bd | |||
8d74023d30 | |||
602625fc17 | |||
bbeb2d5009 | |||
51faa39636 | |||
f37bfbf9f9 | |||
ba9928831e | |||
2b6bd3d751 | |||
e96ca21c89 | |||
6ee10fe98b | |||
a567c19759 | |||
bb3a087d39 | |||
5a92fe736f | |||
88390402a4 | |||
538eb66672 | |||
0b6dfe0fd3 | |||
d5579ef2b5 | |||
836c3a5b3a | |||
f2da64adad | |||
e5704abfb3 | |||
3bf4eea1fe | |||
aa23222339 | |||
68c1670c70 | |||
914eaaaa51 | |||
5831ba2143 | |||
c167a24f09 | |||
a539d27c62 | |||
d7fc079376 | |||
3a05f7e294 | |||
03713f9bd8 | |||
2a145f4350 | |||
d049da696c | |||
5a46d0e80d | |||
926250a967 | |||
139b588795 | |||
909f18f9c7 | |||
f598495198 | |||
95d746504d | |||
9a2e1d43ea | |||
be844978c1 | |||
60a361f963 | |||
93f50451e6 | |||
0936812df0 | |||
50351f56f8 | |||
232ceed8b0 | |||
b6c37a73b1 | |||
5967666df6 | |||
bf035333cf | |||
f93d1173e2 | |||
08bf4faeee | |||
e2b2cf0175 | |||
d32a24004e | |||
1f04e4e6be | |||
778dcf97b1 | |||
957fbdb907 | |||
e169b851ee | |||
7fadb4c5ad | |||
a20db7f341 | |||
b5f4739ae5 | |||
4bc03fbf06 | |||
1d3ff143d2 | |||
6918b8a291 | |||
3cd37682d3 | |||
19a990b095 | |||
87a4f02f18 | |||
8a99fc0505 | |||
bac99deb6c | |||
e65850b1eb | |||
a6e52ed3df | |||
4a9eadf71a | |||
b8f6cf4f23 | |||
e8abc1137b | |||
8507688c50 | |||
5718096224 | |||
d76e61e6f4 | |||
232817c00d | |||
86af585df3 | |||
9e770ea484 | |||
9670f11554 | |||
dc369d52cb | |||
33c755fc54 | |||
c5adc0eb71 | |||
fcb1de8a86 | |||
6df83ad148 | |||
857a436677 | |||
c6091750b0 | |||
64e7324285 | |||
d5bd0ee781 | |||
3b91b38014 | |||
165d4e2732 | |||
098dfacce8 | |||
44d1419af9 | |||
d0d077642d | |||
dc04839fab | |||
4ce0cb4b35 | |||
5100c36c06 | |||
b184360eb7 | |||
02d79de17c | |||
cf27c66132 | |||
53d9ed5adb | |||
3fe5051098 | |||
f7c8a989b6 | |||
65dcfd3549 | |||
6976fc54ca | |||
0e077ff5c4 | |||
c2f171a729 | |||
fea38758e4 | |||
444733565b | |||
96d28f00cc | |||
70cc79a77f | |||
8d10186fdf | |||
6f7e0205f8 | |||
7ef11817c1 | |||
c387c84861 | |||
ae7ad9f667 | |||
c55f1185e6 | |||
1619666bef | |||
bf784f6fd7 | |||
13e330fa65 | |||
827b133534 | |||
4067d4b00f | |||
359d8c5c6a | |||
265b7364e8 | |||
dc2b8c9e4c | |||
37869fd049 | |||
d7ada4d493 | |||
f093f85dbf | |||
1cf17872ab | |||
c79751829b | |||
7a21c03896 | |||
54f07139db | |||
d78990fbd5 | |||
9ed7dbc838 | |||
9b12c7bc57 | |||
8973c75bbc | |||
f425df7b6d | |||
a44b600c5e | |||
7f0a42c2d5 | |||
60cd864226 | |||
71cf02915e | |||
327d2298fb | |||
2ca11ed692 | |||
0224815a60 | |||
df824c36d2 | |||
66e7777b1a | |||
7ff85a86bf | |||
7b3700c2c6 | |||
04679aefd6 | |||
5190639b77 | |||
0bf73abb39 | |||
7e0211924d | |||
a8a857a7ce | |||
1d18965a26 | |||
e020b86a3f | |||
1b80b90609 | |||
efc3512994 | |||
acb8ca982f | |||
adc42cbba4 | |||
7edcb7ef5f | |||
656017c6df | |||
35db6d4a8b | |||
2741187546 | |||
c3a7ab647c | |||
92da0ec2d2 | |||
c767a49f2d | |||
ea8196b532 | |||
58f138e854 | |||
b4b6939498 | |||
bc97c07670 | |||
cf27fe5a53 | |||
449066449b | |||
eb5e32a07f | |||
708cdbe23f | |||
333de52c33 | |||
6b9932fa14 | |||
6c45689e6a | |||
1e3307c84c | |||
d0eed9857d | |||
4221763f48 | |||
184c797b0e | |||
4853e15d8a | |||
6b4b903669 | |||
05da63f2a5 | |||
5b4b073fc8 | |||
4723a83dbb | |||
78350db62d | |||
e8b71f36b2 | |||
6db9061dd1 | |||
320826a4b9 | |||
ad0edb5f4c | |||
2856c10bc3 | |||
24aa18e9ed | |||
767eca97cb | |||
73d5415ea9 | |||
e5a26cfca8 | |||
e40cd1fc0c | |||
978b7d930e | |||
0f2e3ef957 | |||
275b590e80 | |||
5d9da82d8e | |||
1a122726b7 | |||
0bd02a9272 | |||
3cce7b8b35 | |||
e3ab1f5228 | |||
4c875d9c7c | |||
e79334a6f6 | |||
a09c6d51e6 | |||
312c7b7193 | |||
ee733fee28 | |||
4d7e9d3f8a | |||
873c0a183a | |||
ea53ae8f20 | |||
686bc3380d | |||
67da20bcea | |||
561644f75b | |||
1abc89858f | |||
91c63a8ee6 | |||
563882d30b | |||
9a5eeee794 | |||
0578a692db | |||
f74f06338a | |||
1281f348bf | |||
5e76d4bfc1 | |||
2a302ea346 | |||
be90172840 | |||
b85ee895f5 | |||
93de408e07 | |||
d3662ae734 | |||
132d7795ea | |||
bf5a624209 | |||
abbdbda03a | |||
8a8593437a | |||
e203cada54 | |||
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
5c618233cb | |||
9e91259b9e | |||
014d08f38a | |||
7998ea142b | |||
a4051dac72 | |||
e3a8892d24 | |||
ea02d77e69 | |||
4f582a6712 | |||
4769b1d452 | |||
17b18d820f | |||
26f34e75c2 | |||
6f50ac50ec | |||
5261cfcdd3 | |||
675920697f | |||
24699bf2ba | |||
5ab92ed794 | |||
8e83f0faa1 | |||
30d5add2ea | |||
6e47babf45 | |||
9b95fa1f20 | |||
c67aa14a87 | |||
23f296ef34 | |||
c6ce676ad3 | |||
fafb02b0dc | |||
53cecc8c8a | |||
db0e9ee8f8 | |||
2fac794d96 | |||
ffcd716906 | |||
85f50724db | |||
808a995741 | |||
4deb853914 | |||
470ec3354e | |||
053c2da9f1 | |||
c0e28ce66e | |||
08dd94e267 | |||
7497865d1f | |||
1888e4fe2b | |||
6746a5cbd5 | |||
baecb7bb0c | |||
28bf4b42bb | |||
c73dc425ad | |||
2138b7dcb8 | |||
8b6c4a9383 | |||
344755cbd0 | |||
63a975267c | |||
3c7d93e88d | |||
75974037bc | |||
e8a346182b | |||
e96d34f741 | |||
7dad814f19 | |||
7e67ca1413 | |||
603263549b | |||
a82b971ce7 | |||
b58c8ef2f0 | |||
274533bfdf | |||
cd6ce401e1 | |||
0c0809101d | |||
4b342376a8 | |||
fd963b9ad0 | |||
06406c0695 | |||
2b567de5c1 | |||
ef46d03760 | |||
b174f299fa | |||
09837966b9 | |||
465dce1d02 | |||
067dbad546 | |||
522970fdb9 | |||
e67aa499a6 | |||
3b68d81507 | |||
051248f2fc | |||
9a239f99f4 | |||
86c431d66e | |||
b35fe0e8e3 | |||
54905f5ceb | |||
1c9b05d992 | |||
a89c71df38 | |||
3db1f2af12 | |||
5399ff2751 | |||
bcea6027e9 | |||
a9722df7e4 | |||
474be6f7be | |||
ada9a7264b | |||
e991b302d0 | |||
eef301c6ec | |||
3fdfd0adfd | |||
358f1ffc43 | |||
349c3409df | |||
0263a2950c | |||
0364a57cae | |||
e232dd7d7e | |||
fee936b569 | |||
420115c54d | |||
312e961098 | |||
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 | |||
1ef59e05a5 | |||
5e09992637 | |||
30448233b1 | |||
a1601a17aa | |||
7522f7d0f7 | |||
d04b9c4c09 | |||
1a24ff9a49 | |||
13d72de82d | |||
3728fdab3f | |||
2317e3d50c | |||
5f15976c02 | |||
7f592639c5 | |||
a98402af12 | |||
316ffa91d1 | |||
c24953b57e | |||
7a1b1b7e5e | |||
70f71f64c4 | |||
5bccd07d7d | |||
d818baa6d1 | |||
249b8abf03 | |||
c134277514 | |||
f5d366cf7f | |||
ad25a2ed08 | |||
1e7a2ffe97 | |||
dd52075ff1 | |||
0253e42bd5 | |||
d99774f8d9 | |||
d563a2ec89 | |||
b4b4523193 | |||
fbcb69f447 | |||
8ae5a9c1f7 | |||
3ef5bfb6eb | |||
4016ded584 | |||
5b0b4adb1c | |||
f1ec3b0c75 | |||
b5d55a2066 | |||
0d2c9fe377 | |||
2c7cc9a796 | |||
2e1d623755 | |||
52fee8f842 | |||
6ba17e8e30 | |||
ac3432920a | |||
63c88be533 | |||
3cb577e6ba | |||
1e0d64c548 | |||
bc1b9ff59c | |||
7d73bed3be | |||
126fbdfd60 | |||
15094436fd | |||
010c653995 | |||
119f82fd4e | |||
3bbf4de5d2 | |||
0807f3b87b | |||
4e9b3b40aa | |||
cc444811db | |||
50c8525012 | |||
aedad497e8 | |||
b1b231e645 | |||
dc46fd225a | |||
6226de7cff | |||
37327ec674 | |||
c071c81403 | |||
85d75a013a | |||
3816b36131 | |||
dc7965267b | |||
ce9a6bced7 | |||
85325dc710 | |||
ac4050df70 | |||
a16a53167b | |||
afab3cf847 | |||
8fdaeb7bac | |||
7e0f9f6e0d | |||
5b1bf6cd88 | |||
b1584c352b | |||
b06b83503c | |||
b03d89c190 | |||
f53548d10f | |||
5ec2f54d7f | |||
db588ff961 | |||
2b7006a14c | |||
8f5f07882f | |||
0eee8e7464 | |||
3725a5b644 | |||
c84c0ac64d | |||
098e07988c | |||
66bb702aca | |||
03ff2fedf0 | |||
c707f47b11 | |||
585efa3ff5 | |||
07d0b98a23 | |||
c7c0f01010 | |||
cf6b17250a | |||
90503a490c | |||
ebdd53b99b | |||
51a5d2e812 | |||
2ad509d56a | |||
1a98bfba36 | |||
d05bb6c60e | |||
ed81b6a6aa | |||
264914588f | |||
05df43b426 | |||
0334a4e176 | |||
38dca425da | |||
82d4a79dd4 | |||
6725be8145 | |||
f5b693f01b | |||
f09f23e570 | |||
4f4d05b8cd | |||
0c5b5ff49c | |||
a815fad3f1 | |||
d8b1c7c10a | |||
02e1aea80c | |||
1892f7e0f4 | |||
b7b50349a7 | |||
02d227ee02 | |||
47f8938b89 | |||
4945a640a7 | |||
0136977359 | |||
0acd3e20b0 | |||
30bdfeee37 | |||
7ea665d884 | |||
073edcfb12 | |||
a645366a25 | |||
12aa0b7abd | |||
3f98a50410 | |||
24c8c076d5 | |||
37e6931d33 | |||
86493568e9 | |||
bb51436ae3 | |||
854a55ac1a | |||
cfb4b080d3 | |||
00aa2e4e17 | |||
69c67d99f6 | |||
65596ec8c1 | |||
49643cb00e | |||
35b0faee57 | |||
88ef4d69b2 | |||
575b6ca222 | |||
b5a0e844d2 | |||
2642e11ce2 | |||
b4fe655efe | |||
ffb761909a | |||
b443e1ac6e | |||
a4792f54a7 | |||
686ae029e0 | |||
f3fd2e7d0f | |||
7efd9ba0a5 | |||
1c2a6bb8a1 | |||
7bcf1cbdd5 | |||
2aaa2544bd | |||
d85f03ba20 | |||
cfb51a6be4 | |||
c9d778c94b | |||
fd62f882de | |||
adc050f190 | |||
2d551b9fc5 | |||
884acdde32 | |||
8f896de794 | |||
5e4e26d2fd | |||
ae688e6615 | |||
c4c812bdf6 | |||
e620fc0283 | |||
c333902468 | |||
4c83ecd06a | |||
b28a547dc4 | |||
6bc17e05bd | |||
0903350d30 | |||
6c0f19b457 | |||
e119dc823f | |||
43295c9c57 | |||
ded8b54042 | |||
50a3178d51 | |||
393c226032 | |||
f2630df387 | |||
abcd2c1750 | |||
cc95f3b5b5 | |||
a08ee93b43 | |||
4b90f873d5 | |||
419ab8e0b1 | |||
c95ef27998 | |||
63dfd93834 | |||
57610881de | |||
7469faf296 | |||
55a884a559 | |||
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 | |||
964e541c32 | |||
78fec4ed22 | |||
ef111d36c9 | |||
4f64193e85 | |||
89bb6d1268 | |||
9f4226bf0f | |||
a87c2a3374 | |||
d7294ba5a0 | |||
82d286dc6f | |||
1fa18ab997 | |||
afc90f32c9 | |||
e9cfb7c21e | |||
1af8ea3769 | |||
9f7af190f1 | |||
9c703fe94d | |||
a7a11a4f13 | |||
c32c3bb62b | |||
e29d1480a6 | |||
8f299d7791 | |||
65fb2e992e | |||
41f5d677d5 | |||
a2b78b8cd9 | |||
c93f217033 | |||
82c47b6e9a | |||
94fb738c67 | |||
89071e40fc | |||
95a90c410e | |||
59fc371cd5 | |||
9b404e330d | |||
1667f9b2ef | |||
caadfc8641 | |||
bffc2e70c1 | |||
8b686f0b12 | |||
def8d1e0cb | |||
ca28c34be0 | |||
196bc3ea00 | |||
b15267be4d | |||
5c074f6f5f | |||
04cba61888 | |||
a41e2e1ceb | |||
d1d03c98ba | |||
679942159e | |||
3e48a54ab5 | |||
f6e389ff62 | |||
63c309bd12 | |||
561ec57cc8 | |||
3cefd7bd1e | |||
c63feb488c | |||
12c418d84d | |||
4b982f815c | |||
d4d3346b6d | |||
6010a103e0 | |||
5dc1da2af0 | |||
f2ccc4d963 | |||
a92d48efdd | |||
b633206b45 | |||
b6f3d2af5e | |||
5fd77d9fcc | |||
5ca4494eed | |||
de7e419ef4 | |||
20a6b3fc33 | |||
540414d8f5 | |||
abcdb8ced0 | |||
5076d73695 | |||
d88735f84e | |||
2244f0ab76 | |||
40c85d6104 | |||
42892e24f4 | |||
e6357d2ac8 | |||
1eecd85ceb | |||
c27557826b | |||
88150b6535 | |||
d63176da19 | |||
887da5aa9a | |||
6e7f1151bc | |||
b2aebcc5d3 | |||
ae9ad0fa65 | |||
fb6d852827 | |||
ba17612461 | |||
a15c7a0213 | |||
7e321d4016 | |||
a05cd5678b | |||
2ccf007b9a | |||
895b8c2c80 | |||
0f175174f6 | |||
493466683c | |||
761c342c51 | |||
5341da28d9 | |||
7768f41849 | |||
fa8993191e | |||
239ce28575 | |||
c52a49f747 | |||
e4b9895ba7 | |||
92a2bb4d32 | |||
bfec722312 | |||
5a3f7b5b70 | |||
2aa097be46 | |||
8a646d85c6 | |||
890b3eaa00 | |||
cda28ebf15 | |||
2245027ca3 | |||
3dc250f801 | |||
66e786a1b0 | |||
1e26926350 | |||
8bd7ea5bbc | |||
6eb36abe2e | |||
6fced3fab2 | |||
774e456e54 | |||
519859e1c5 | |||
26f0c488e5 | |||
1b0b53fbd0 | |||
35f4ea29f9 | |||
fff39d9879 | |||
444e761d41 | |||
68a3def35a | |||
d9426d301d | |||
8bcf7109a3 | |||
3effdf0f4d | |||
3c122bcf53 | |||
037ff52f4f | |||
b11f8acba1 | |||
ef9a633aa4 | |||
c7e2f979dd | |||
e97bb9c933 | |||
fa506b5bf8 | |||
e3193a92d0 | |||
d76dabdca6 | |||
d219f50912 | |||
e08710a19c | |||
4e167b35be | |||
16873384a8 | |||
f87339f9fa | |||
5f5e5e3211 | |||
f724db8226 | |||
c7e90cd7df | |||
8c5b00b1a3 | |||
a3b79fbcd8 | |||
9db77e6351 | |||
5bc1eaec9f | |||
81c9ce7284 | |||
caa6978d80 | |||
af22d6a4e3 | |||
0eabb3c37c | |||
2b84791391 | |||
6f896cb096 | |||
9a488c60f2 | |||
9cb50446f4 | |||
8c00a2359e | |||
d1ff34d16d | |||
8e8615dab8 | |||
a63ed4d3b4 | |||
968c820702 | |||
3061b4dfd2 | |||
ed4de612dd | |||
d4bdd5fd9c | |||
8b71556425 | |||
ae6e1bfd85 | |||
6d1f3b73ef | |||
0dcaf80c7f | |||
fc9cd5bdf0 | |||
a434c45196 | |||
87b316ec23 | |||
9c99ffae57 | |||
30a3a84ec9 | |||
d0f585df9d | |||
bac2db5cda | |||
c35bf2f483 | |||
2e04c5e39c | |||
9dcf16e819 | |||
361d494cde | |||
4d7015294e | |||
5f16fb4668 | |||
4bf2228675 | |||
2ba823f192 | |||
27fa2d5b69 | |||
47ef7661d8 | |||
3f6ff25322 | |||
f56c23009a | |||
e80593fb7b | |||
57324345ac | |||
73e280157d | |||
cfaa5766ed | |||
8b08db308b | |||
3e2ff55954 | |||
70d1d0d230 | |||
94e0048a3b | |||
a34d1641b3 | |||
40c645e433 | |||
b2e5415a35 | |||
365ee4cf0b | |||
2b4603a234 | |||
ec23eae21d | |||
7a9229628a | |||
9db5c0f375 | |||
27bde55f54 | |||
2bb24282d2 | |||
998472e463 | |||
63ff46a768 | |||
660f43e3b7 | |||
0ba96aa4b8 | |||
d85247d2ad | |||
9ca85ed365 | |||
93113fd871 | |||
b5d360594a | |||
d5ae79c38c | |||
7cf07b27e3 | |||
bb0f986b0c | |||
2c2a85327f | |||
7bf03e497b | |||
7a4dee3d38 | |||
7b27d6f0bb | |||
83dc95a0a7 | |||
d60889f952 | |||
8c9952973d | |||
00673bdb7f | |||
d039890a9b | |||
41e88c07fe | |||
67c5027b16 | |||
a341d4f800 | |||
e3833914b3 | |||
49cdec6961 | |||
3ad1834439 | |||
4b492eae85 | |||
f0ff47af8d | |||
991826b686 | |||
22d59a1ed7 | |||
475ea68696 | |||
864e84706a | |||
9c93e76eeb | |||
7fa1b65af0 | |||
c00c95efcf | |||
94be2b46d5 | |||
4b4d0d2d19 | |||
0d06cf63b7 | |||
7b24c02d51 | |||
becf488714 | |||
e89e8226e4 | |||
a533a96598 | |||
27321c0919 | |||
058472d325 | |||
b5c9a03052 | |||
07dad3affa | |||
8afc103ae7 | |||
591d7b4b80 | |||
2162afc78e | |||
25e226d219 | |||
8472bfe90d | |||
93645b2fbe | |||
d53c987f2e | |||
682693a9f0 | |||
e836faf792 | |||
6e27233be8 | |||
9209984a2f | |||
1477630c78 | |||
ab670080c7 | |||
8198f98376 | |||
65b4697229 | |||
e75a1a8b70 | |||
580494fea7 | |||
5a958da84d | |||
cad602ad14 | |||
1f14bd6188 | |||
156f52b76f | |||
d674b8ac71 | |||
861150971f | |||
a653421514 | |||
8f234a02cb | |||
92ecf99427 | |||
705dbf12d7 | |||
fe11b11c13 | |||
f2a43ad1f3 | |||
cbbe5cfb25 | |||
0eccc6085b | |||
a89da1f705 | |||
5b297e539a | |||
1d932c3753 | |||
5a77fc74ba | |||
7b47b96252 | |||
a4bec83ecc | |||
8509a0de18 | |||
8e30b7430d | |||
9235d32a45 | |||
dd503570ac | |||
613281a1e7 | |||
bab7bf6633 | |||
1831692761 | |||
e144d2479b | |||
c25831316e | |||
60b72aabe8 | |||
c8fcb0ab18 | |||
9911d18390 | |||
e24630ac1e | |||
4c1fd3edae | |||
f65492dd66 | |||
5d978c7670 | |||
11788cece9 | |||
1aaa55dc62 | |||
ce57a2b8fb | |||
0604cc5bd0 | |||
3d2c0bcc6c | |||
0f222979a6 | |||
a1eb6a14f5 | |||
186ce01022 | |||
0096ec1d12 | |||
2929d7bf51 | |||
d90fb5764d | |||
4dccd0c733 | |||
300d912331 | |||
9d21c89151 | |||
24a8c4015c | |||
5eb40d6b7f | |||
36f486e91b | |||
5b684ac26e | |||
85062725bd | |||
401d9c8565 | |||
6f276ac1bc | |||
4350785cef | |||
9a2a85ac3d | |||
d030a61322 | |||
dacb6dca41 | |||
c40fc69087 | |||
eff983135c | |||
479303dd9e | |||
e9b2088f7d | |||
4af5b94013 | |||
441398402d | |||
258d4fda3f | |||
8e667f6c3f | |||
a996cc2e6d | |||
9b8a8690e7 | |||
f2387fd6b5 | |||
888036a99d | |||
539c0ed7f0 | |||
95e065a462 | |||
087f20cb6c | |||
7adf321956 | |||
dc749462ec | |||
16b57f24a2 | |||
b16b1c3e8b | |||
fee56873b5 | |||
e1b2b72cd2 | |||
daf4e5ce6c | |||
2ec2c7263f | |||
abfcab552f | |||
cfdf8b1670 | |||
f23e2a3ec4 | |||
aa1ac3da50 | |||
c9c7316b7d | |||
d152d5cd90 | |||
6fd37710e1 | |||
0419a3c19a | |||
0c382da561 | |||
9fc7f287d2 | |||
dd7c4850f0 | |||
93992ec3ed | |||
15d9adfbf1 | |||
676a914c40 | |||
b423b4eec1 | |||
9784a89112 | |||
7b596e6d9c | |||
76febcf238 | |||
a57a72de88 | |||
235b307b06 | |||
05b0f6d0f7 | |||
1d7081d8b8 | |||
c0174c0c2c | |||
fa8324c1f9 | |||
4b0951caec | |||
0d51c99717 | |||
24623c59d7 | |||
88044f6b76 | |||
38edbf8362 | |||
bc0acf5701 | |||
a82f181126 | |||
be0139a46f | |||
4db5b4f2b1 | |||
93cefced80 | |||
85f586f623 | |||
2be1f97419 | |||
63014231ab | |||
3ac37497ab | |||
d0cafb020f | |||
d3b3198b68 | |||
c1f17ff63b | |||
dafd958f69 | |||
f51af6c61c | |||
254db22063 | |||
8be4256278 | |||
8e8669d63f | |||
4625ff92f1 | |||
6aa84326af | |||
9a384d81fe | |||
0cbe36c048 | |||
7f16aa8c7e | |||
872f8a6229 | |||
9b261daa6d | |||
c46c15c258 |
102
.circleci/config.yml
Normal file
102
.circleci/config.yml
Normal file
@ -0,0 +1,102 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
machine:
|
||||
docker_layer_caching: true
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
test:
|
||||
machine:
|
||||
docker_layer_caching: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
cd BTCPayServer.Tests
|
||||
docker-compose down --v
|
||||
docker-compose build
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="true"
|
||||
if [ -n "$CIRCLE_PULL_REQUEST" ] || [ -n "$CIRCLE_PR_NUMBER" ]; then
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="false"
|
||||
fi
|
||||
docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests
|
||||
|
||||
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||
publish_docker_linuxamd64:
|
||||
machine:
|
||||
docker_layer_caching: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
|
||||
|
||||
publish_docker_linuxarm:
|
||||
machine:
|
||||
docker_layer_caching: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||
|
||||
publish_docker_multiarch:
|
||||
machine:
|
||||
enabled: true
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
# Turn on Experimental features
|
||||
sudo mkdir $HOME/.docker
|
||||
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
|
||||
#
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
#
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
|
||||
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- test
|
||||
|
||||
publish:
|
||||
jobs:
|
||||
- publish_docker_linuxamd64:
|
||||
filters:
|
||||
# ignore any commit on any branch by default
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# only act on version tags
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
- publish_docker_linuxarm:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
- publish_docker_multiarch:
|
||||
requires:
|
||||
- publish_docker_linuxamd64
|
||||
- publish_docker_linuxarm
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Report a problem
|
||||
about: File a technical problem or report a bug
|
||||
---
|
||||
|
||||
**Describe the problem/bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Your environment**
|
||||
* Version of BTCPay Server:
|
||||
* Deployment method:
|
||||
* Other relevant environment details:
|
||||
|
||||
**Logs (if applicable)**
|
||||
Basic logs can be found in Server Settings > Logs.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
Tell us what happens instead
|
||||
|
||||
**Screenshots/Links**
|
||||
If applicable, add screenshots or links to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Ideas and feature requests
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Provide examples**
|
||||
If applicable provide examples, wireframes, sketches or images to better explain your idea.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -6,12 +6,13 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
@ -24,9 +25,13 @@
|
||||
<None Update="docker-compose.yml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,4 +1,6 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Linq;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -32,6 +34,8 @@ using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -106,7 +110,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
|
||||
@ -117,15 +120,18 @@ namespace BTCPayServer.Tests
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
HttpClient = new HttpClient();
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.UseContentRoot(FindBTCPayServerDirectory())
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
@ -138,6 +144,12 @@ namespace BTCPayServer.Tests
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
if (MockRates)
|
||||
{
|
||||
@ -200,6 +212,14 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private string FindBTCPayServerDirectory()
|
||||
{
|
||||
var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
|
||||
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
@ -207,6 +227,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public BTCPayNetworkProvider Networks { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -68,6 +69,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -83,6 +85,7 @@ namespace BTCPayServer.Tests
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
@ -104,6 +107,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -114,16 +118,13 @@ namespace BTCPayServer.Tests
|
||||
var changellyController =
|
||||
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
|
||||
changellyController.IsTest = true;
|
||||
|
||||
|
||||
//test non existing payment method
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel
|
||||
{
|
||||
Enabled = false
|
||||
};
|
||||
var updateModel = CreateDefaultChangellyParams(false);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
@ -142,7 +143,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
|
||||
Assert.IsNotType<BitpayErrorModel>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
@ -150,8 +150,19 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
|
||||
{
|
||||
return new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
|
||||
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
|
||||
ChangellyMerchantId = "804298eb5753",
|
||||
Enabled = enabled
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanGetCurrencyListFromChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -161,32 +172,30 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
|
||||
//save changelly settings
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
//confirm saved
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository,httpClientFactory), tester.NetworkProvider, fetcher);
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
var result = Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value as IEnumerable<CurrencyFull>;
|
||||
Assert.True(result.Any());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanCalculateToAmountForChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
@ -195,10 +204,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
@ -208,20 +214,40 @@ namespace BTCPayServer.Tests
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository,httpClientFactory), tester.NetworkProvider, fetcher);
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m)).Value);
|
||||
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeBaseAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
|
||||
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
|
||||
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
|
||||
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCorrectAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
|
||||
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
|
||||
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
public class MockHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient();
|
||||
return new HttpClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CoinSwitchTests
|
||||
{
|
||||
public CoinSwitchTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.Null(storeBlob.CoinSwitchSettings);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
|
||||
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
|
||||
updateModel.MerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
304
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
304
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.UnitTest1;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CrowdfundTests
|
||||
{
|
||||
public CrowdfundTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateAndDeleteCrowdfundApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
var apps = user.GetController<AppsController>();
|
||||
var apps2 = user2.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanContributeOnlyWhenAllowed()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
//Scenario 1: Not Enabled - Not Allowed
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}, default));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}, default));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCrowdfundModel()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
Logs.Tester.LogInformation("We create an invoice with a hardcap");
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
|
||||
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
|
||||
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
|
||||
Assert.Equal(0m, model.Info.CurrentAmount );
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount);
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
|
||||
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
|
||||
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
|
||||
crowdfundViewModel.EnforceTargetAmount = false;
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Threading.Channels;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
TaskCompletionSource<bool> _Evt = null;
|
||||
IWebHost _Host = null;
|
||||
CancellationTokenSource _Closed = new CancellationTokenSource();
|
||||
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
|
||||
public CustomServer()
|
||||
{
|
||||
{
|
||||
var port = Utils.FreeTcpPort();
|
||||
_Host = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(req =>
|
||||
{
|
||||
while (_Act == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
_Closed.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
_Act(req);
|
||||
_Act = null;
|
||||
_Evt.TrySetResult(true);
|
||||
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
public async Task<JObject> GetNextRequest()
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>();
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>();
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
|
||||
{
|
||||
_Evt.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
try
|
||||
{
|
||||
JObject req = null;
|
||||
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
|
||||
!_Requests.Reader.TryRead(out req))
|
||||
{
|
||||
|
||||
}
|
||||
return req;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
|
||||
WORKDIR /app/BTCPayServer.Tests
|
||||
RUN dotnet restore
|
||||
# copies the rest of your code
|
||||
COPY . ../.
|
||||
|
||||
ENTRYPOINT ["dotnet", "test"]
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
COPY . .
|
||||
RUN dotnet build
|
||||
WORKDIR /source/BTCPayServer.Tests
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
26
BTCPayServer.Tests/Extensions.cs
Normal file
26
BTCPayServer.Tests/Extensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static T AssertViewModel<T>(this IActionResult result)
|
||||
{
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
public static async Task<T> AssertViewModelAsync<T>(this Task<IActionResult> task)
|
||||
{
|
||||
var result = await task;
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
}
|
||||
}
|
67
BTCPayServer.Tests/MockDelay.cs
Normal file
67
BTCPayServer.Tests/MockDelay.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class MockDelay : IDelay
|
||||
{
|
||||
class WaitObj
|
||||
{
|
||||
public DateTimeOffset Expiration;
|
||||
public TaskCompletionSource<bool> CTS;
|
||||
}
|
||||
|
||||
List<WaitObj> waits = new List<WaitObj>();
|
||||
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
|
||||
{
|
||||
WaitObj w = new WaitObj();
|
||||
w.Expiration = _Now + delay;
|
||||
w.CTS = new TaskCompletionSource<bool>();
|
||||
using (cancellation.Register(() =>
|
||||
{
|
||||
w.CTS.TrySetCanceled();
|
||||
}))
|
||||
{
|
||||
lock (waits)
|
||||
{
|
||||
waits.Add(w);
|
||||
}
|
||||
await w.CTS.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
List<WaitObj> overdue = new List<WaitObj>();
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
overdue.Add(wait);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var o in overdue)
|
||||
o.CTS.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -10,7 +11,7 @@ namespace BTCPayServer.Tests.Mocks
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
|
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PSBTTests
|
||||
{
|
||||
public PSBTTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPlayWithPSBT()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
|
||||
Amount = 0.1m,
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmLedger.SuccessPath);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletSend(walletId, sendModel, command: "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Errors);
|
||||
Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.ExtKey);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
// Can use uploaded file?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
216
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
216
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PaymentRequestTests
|
||||
{
|
||||
public PaymentRequestTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateViewUpdateAndDeletePaymentRequest()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var id = (Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||
|
||||
|
||||
|
||||
//permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(id));
|
||||
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||
|
||||
//Delete
|
||||
|
||||
Assert.IsType<ConfirmModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||
|
||||
Assert
|
||||
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(1, invoice.Price);
|
||||
|
||||
request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice with expiry",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
|
||||
response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
Assert
|
||||
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCancelPaymentWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var paymentRequestId = response.Value.ToString();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status);
|
||||
Assert.IsType<OkObjectResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status);
|
||||
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -29,12 +29,6 @@ If you want to stop, and remove all existing data
|
||||
docker-compose down --v
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
|
||||
|
||||
## How to manually test payments
|
||||
@ -47,10 +41,15 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
```powershell
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
You can also generate blocks:
|
||||
```powershell
|
||||
.\docker-bitcoin-generate.ps1 3
|
||||
```
|
||||
|
||||
### Using the test litecoin-cli
|
||||
|
||||
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
|
||||
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Tests
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void SecondDuplicatedRuleIsIgnored()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
@ -24,6 +25,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
// Check happy path
|
||||
|
@ -22,6 +22,8 @@ using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -43,6 +45,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
ExplorerNode.ScanRPCCapabilities();
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
@ -84,9 +87,11 @@ namespace BTCPayServer.Tests
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task EnsureConnectedToDestinations()
|
||||
public async Task EnsureChannelsSetup()
|
||||
{
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
Logs.Tester.LogInformation("Connecting channels");
|
||||
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
|
||||
Logs.Tester.LogInformation("Channels connected");
|
||||
}
|
||||
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
@ -152,6 +157,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
|
237
BTCPayServer.Tests/StorageTests.cs
Normal file
237
BTCPayServer.Tests/StorageTests.cs
Normal file
@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
using BTCPayServer.Storage.ViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class StorageTests
|
||||
{
|
||||
public StorageTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanConfigureStorage()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
|
||||
// //For some reason, the tests cache something on circleci and this is set by default
|
||||
// //Initially, there is no configuration, make sure we display the choices available to configure
|
||||
// Assert.IsType<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
|
||||
//
|
||||
// //the file list should tell us it's not configured:
|
||||
// var viewFilesViewModelInitial =
|
||||
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
|
||||
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
|
||||
|
||||
|
||||
//Once we select a provider, redirect to its view
|
||||
var localResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.FileSystem
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
|
||||
|
||||
|
||||
var AmazonS3result = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.AmazonS3
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName);
|
||||
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
|
||||
|
||||
var GoogleResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.GoogleCloudStorage
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName);
|
||||
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
|
||||
|
||||
|
||||
var AzureResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.AzureBlobStorage
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName);
|
||||
Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]);
|
||||
|
||||
//Cool, we get redirected to the config pages
|
||||
//Let's configure this stuff
|
||||
|
||||
//Let's try and cheat and go to an invalid storage provider config
|
||||
Assert.Equal(nameof(Storage), (Assert
|
||||
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
|
||||
.ActionName));
|
||||
|
||||
//ok no more messing around, let's configure this shit.
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
|
||||
.Model);
|
||||
|
||||
//local file system does not need config, easy days!
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
|
||||
|
||||
//ok cool, let's see if this got set right
|
||||
var shouldBeRedirectingToLocalStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
//if we tell the settings page to force, it should allow us to select a new provider
|
||||
Assert.IsType<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
|
||||
|
||||
//awesome, now let's see if the files result says we're all set up
|
||||
var viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
|
||||
Assert.True(viewFilesViewModel.StorageConfigured);
|
||||
Assert.Empty(viewFilesViewModel.Files);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanUseLocalProviderFiles()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
|
||||
.Model);
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
|
||||
|
||||
var shouldBeRedirectingToLocalStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("ExternalIntegration", "ExternalIntegration")]
|
||||
public async Task CanUseAzureBlobStorage()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
|
||||
.Model);
|
||||
|
||||
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
|
||||
azureBlobStorageConfiguration.ContainerName = "testscontainer";
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
|
||||
|
||||
|
||||
var shouldBeRedirectingToAzureStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.AzureBlobStorage,
|
||||
shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
//seems like azure config worked, let's see if the conn string was actually saved
|
||||
|
||||
Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert
|
||||
.IsType<AzureBlobStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(
|
||||
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
|
||||
.Model).ConnectionString);
|
||||
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task CanUploadRemoveFiles(ServerController controller)
|
||||
{
|
||||
var fileContent = "content";
|
||||
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
|
||||
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
|
||||
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
|
||||
Assert.Equal("Files", uploadFormFileResult.ActionName);
|
||||
|
||||
var viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
|
||||
|
||||
Assert.NotEmpty(viewFilesViewModel.Files);
|
||||
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
|
||||
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
|
||||
|
||||
|
||||
var net = new System.Net.WebClient();
|
||||
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
|
||||
Assert.Equal(fileContent, data);
|
||||
|
||||
Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert
|
||||
.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId))
|
||||
.RouteValues["statusMessage"].ToString()).Severity);
|
||||
|
||||
viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
|
||||
|
||||
Assert.Null(viewFilesViewModel.DirectFileUrl);
|
||||
Assert.Null(viewFilesViewModel.SelectedFileId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static string GetFromSecrets(string key)
|
||||
{
|
||||
var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}");
|
||||
if (!string.IsNullOrEmpty(connStr) && connStr != "none")
|
||||
return connStr;
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
|
||||
var config = builder.Build();
|
||||
var token = config[key];
|
||||
Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} <value>\"");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -58,6 +59,21 @@ namespace BTCPayServer.Tests
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void SetNetworkFeeMode(NetworkFeeMode mode)
|
||||
{
|
||||
ModifyStore((store) =>
|
||||
{
|
||||
store.NetworkFeeMode = mode;
|
||||
});
|
||||
}
|
||||
public void ModifyStore(Action<StoreViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<StoresController>();
|
||||
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
|
||||
modify(store);
|
||||
storeController.UpdateStore(store).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
@ -73,20 +89,16 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public WalletId RegisterDerivationScheme(string crytoCode)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
|
||||
{
|
||||
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
@ -101,15 +113,18 @@ namespace BTCPayServer.Tests
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
var account = parent.PayTester.GetController<AccountController>();
|
||||
await account.Register(new RegisterViewModel()
|
||||
RegisterDetails = new RegisterViewModel()
|
||||
{
|
||||
Email = Guid.NewGuid() + "@toto.com",
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
});
|
||||
};
|
||||
await account.Register(RegisterDetails);
|
||||
UserId = account.RegisteredUserId;
|
||||
}
|
||||
|
||||
public RegisterViewModel RegisterDetails{ get; set; }
|
||||
|
||||
public Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
|
81
BTCPayServer.Tests/TestUtils.cs
Normal file
81
BTCPayServer.Tests/TestUtils.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class TestUtils
|
||||
{
|
||||
public static FormFile GetFormFile(string filename, string content)
|
||||
{
|
||||
File.WriteAllText(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "text/plain";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static FormFile GetFormFile(string filename, byte[] content)
|
||||
{
|
||||
File.WriteAllBytes(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "application/octet-stream";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
111
BTCPayServer.Tests/UtilitiesTests.cs
Normal file
111
BTCPayServer.Tests/UtilitiesTests.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using System.IO;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// This class hold easy to run utilities for dev time
|
||||
/// </summary>
|
||||
public class UtilitiesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
|
||||
/// </summary>
|
||||
[Trait("Utilities", "Utilities")]
|
||||
[Fact]
|
||||
public async Task PullTransifexTranslations()
|
||||
{
|
||||
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
|
||||
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
|
||||
var client = new TransifexClient(GetTransifexAPIToken());
|
||||
var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/");
|
||||
var langs = new[] { "en" }.Concat(((JObject)json["stats"]).Properties().Select(n => n.Name)).ToArray();
|
||||
|
||||
var langsDir = Path.Combine(Services.LanguageService.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
|
||||
|
||||
JObject sourceLang = null;
|
||||
Task.WaitAll(langs.Select(async l =>
|
||||
{
|
||||
bool isSourceLang = l == "en";
|
||||
var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/");
|
||||
if(!isSourceLang)
|
||||
{
|
||||
while (sourceLang == null)
|
||||
await Task.Delay(10);
|
||||
}
|
||||
var content = j["content"].Value<string>();
|
||||
if (l == "ne_NP")
|
||||
l = "np_NP";
|
||||
if (l == "zh_CN")
|
||||
l = "zh-SP";
|
||||
if (l == "kk")
|
||||
l = "kk-KZ";
|
||||
|
||||
var langCode = l.Replace("_", "-");
|
||||
var langFile = Path.Combine(langsDir, langCode + ".json");
|
||||
var jobj = JObject.Parse(content);
|
||||
jobj["code"] = langCode;
|
||||
|
||||
if ((string)jobj["currentLanguage"] == "English" && !isSourceLang)
|
||||
return; // Not translated
|
||||
if ((string)jobj["currentLanguage"] == "disable")
|
||||
return; // Not translated
|
||||
|
||||
jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
|
||||
if (isSourceLang)
|
||||
{
|
||||
sourceLang = jobj;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(jobj["InvoiceExpired_Body_3"].Value<string>() == sourceLang["InvoiceExpired_Body_3"].Value<string>())
|
||||
{
|
||||
jobj["InvoiceExpired_Body_3"] = string.Empty;
|
||||
}
|
||||
}
|
||||
content = jobj.ToString(Newtonsoft.Json.Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(langsDir, langFile), content);
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private static string GetTransifexAPIToken()
|
||||
{
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
|
||||
var config = builder.Build();
|
||||
var token = config["TransifexAPIToken"];
|
||||
Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken <youapitoken>\"");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public class TransifexClient
|
||||
{
|
||||
public TransifexClient(string apiToken)
|
||||
{
|
||||
Client = new HttpClient();
|
||||
APIToken = apiToken;
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
public string APIToken { get; }
|
||||
|
||||
public async Task<JObject> GetTransifexAsync(string uri)
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes($"api:{APIToken}")));
|
||||
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
var response = await Client.SendAsync(message);
|
||||
return await response.Content.ReadAsAsync<JObject>();
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
|
||||
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
|
||||
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
|
||||
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
|
||||
docker exec -ti "$bitcoind_container_id" bitcoin-cli -datadir="/data" "$@"
|
||||
|
3
BTCPayServer.Tests/docker-bitcoin-generate.ps1
Normal file
3
BTCPayServer.Tests/docker-bitcoin-generate.ps1
Normal file
@ -0,0 +1,3 @@
|
||||
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
|
||||
$address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress)
|
||||
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress $args $address
|
@ -2,7 +2,7 @@ version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
tests:
|
||||
@ -19,10 +19,12 @@ services:
|
||||
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
|
||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
|
||||
TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/"
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
@ -36,7 +38,7 @@ services:
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -53,7 +55,7 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -69,7 +71,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.1.0.4
|
||||
image: nicolasdorier/nbxplorer:2.0.0.40
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -93,22 +95,24 @@ services:
|
||||
- bitcoind
|
||||
- litecoind
|
||||
|
||||
|
||||
bitcoind:
|
||||
image: nicolasdorier/docker-bitcoin:0.17.0
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
rpcport=43782
|
||||
rpcbind=0.0.0.0:43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
deprecatedrpc=signrawtransaction
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
@ -118,7 +122,8 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -143,7 +148,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.3
|
||||
image: shesek/lightning-charge:0.4.6-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
@ -163,7 +168,8 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6.1-1-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -186,13 +192,13 @@ services:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
image: nicolasdorier/docker-litecoin:0.15.1
|
||||
restart: unless-stopped
|
||||
image: nicolasdorier/docker-litecoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
@ -219,13 +225,16 @@ services:
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
image: btcpayserver/lnd:v0.6-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXPLORERURL: "http://nbxplorer:32838/"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
rpclisten=127.0.0.1:10008
|
||||
rpclisten=0.0.0.0:10009
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
@ -246,13 +255,16 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
image: btcpayserver/lnd:v0.6-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXPLORERURL: "http://nbxplorer:32838/"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
rpclisten=127.0.0.1:10008
|
||||
rpclisten=0.0.0.0:10009
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
|
||||
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
|
||||
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli "$@"
|
||||
|
8
BTCPayServer.Tests/docker-entrypoint.sh
Executable file
8
BTCPayServer.Tests/docker-entrypoint.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
dotnet test --filter Fast=Fast --no-build
|
||||
dotnet test --filter Integration=Integration --no-build -v n
|
||||
if [[ "$TESTS_RUN_EXTERNAL_INTEGRATION" == "true" ]]; then
|
||||
dotnet test --filter ExternalIntegration=ExternalIntegration --no-build -v n
|
||||
fi
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
|
||||
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
|
||||
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
|
||||
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
|
||||
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"
|
||||
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
|
||||
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
|
||||
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"
|
||||
|
5
BTCPayServer.Tests/xunit.runner.json
Normal file
5
BTCPayServer.Tests/xunit.runner.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"parallelizeTestCollections": false,
|
||||
"longRunningTestSeconds": 60,
|
||||
"diagnosticMessages": true
|
||||
}
|
@ -8,10 +8,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitTokenEntity
|
||||
{
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
@ -39,7 +35,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = Label,
|
||||
Facade = Facade,
|
||||
StoreId = StoreId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
|
@ -11,11 +11,6 @@ namespace BTCPayServer.Authentication
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using BTCPayServer.Data;
|
||||
using DBreeze;
|
||||
using DBriize;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
@ -90,7 +90,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = data.Label,
|
||||
Facade = data.Facade,
|
||||
Value = data.Id,
|
||||
SIN = data.SIN,
|
||||
PairingTime = data.PairingTime,
|
||||
@ -129,7 +128,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
@ -178,7 +176,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
@ -213,7 +210,6 @@ namespace BTCPayServer.Authentication
|
||||
return null;
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
Id = data.Id,
|
||||
Label = data.Label,
|
||||
Expiration = data.Expiration,
|
||||
@ -242,6 +238,8 @@ namespace BTCPayServer.Authentication
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if (token == null)
|
||||
return null;
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,12 @@ using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public enum DerivationType
|
||||
{
|
||||
Legacy,
|
||||
SegwitP2SH,
|
||||
Segwit
|
||||
}
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
@ -44,7 +50,6 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
@ -64,7 +69,8 @@ namespace BTCPayServer
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public bool SupportRBF { get; internal set; }
|
||||
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
|
@ -25,7 +25,22 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
|
||||
SupportRBF = true,
|
||||
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy }, // xpub
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
|
||||
{0x4b24746U, DerivationType.Segwit }, //zpub
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy},
|
||||
{0x044a5262U, DerivationType.SegwitP2SH},
|
||||
{0x045f1cf6U, DerivationType.Segwit}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
35
BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinplus()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcoinplus",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoinplus",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"XBC_X = XBC_BTC * BTC_X",
|
||||
"XBC_BTC = cryptopia(XBC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/bitcoinplus.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/BTCPayNetworkProvider.Bitcore.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Bitcore.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcore()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTX");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcore",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcore",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTX_X = BTX_BTC * BTC_X",
|
||||
"BTX_BTC = hitbtc(BTX_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/bitcore.svg",
|
||||
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -27,8 +27,7 @@ namespace BTCPayServer
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
|
||||
: new KeyPath("1'"),
|
||||
MinFee = Money.Satoshis(1m)
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ namespace BTCPayServer
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,23 +10,24 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Groestlcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
|
||||
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
|
@ -17,14 +17,30 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://live.blockcypher.com/ltc/tx/{0}/"
|
||||
: "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
|
||||
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy },
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH },
|
||||
{0x04b24746U, DerivationType.Segwit },
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy },
|
||||
{0x044a5262U, DerivationType.SegwitP2SH },
|
||||
{0x045f1cf6U, DerivationType.Segwit }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = zaif(MONA_BTC)"
|
||||
"MONA_BTC = bittrex(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
|
@ -47,14 +47,34 @@ namespace BTCPayServer
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitBitcore();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks)
|
||||
{
|
||||
if(network.Value.ElectrumMapping.Count == 0)
|
||||
{
|
||||
network.Value.ElectrumMapping = GetNetwork("BTC").ElectrumMapping;
|
||||
if (!network.Value.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
{
|
||||
network.Value.ElectrumMapping =
|
||||
network.Value.ElectrumMapping
|
||||
.Where(kv => kv.Value == DerivationType.Legacy)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.1</Version>
|
||||
<Version>1.0.3.101</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -30,40 +30,52 @@
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.18" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.66" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.3.3" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
|
||||
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
|
||||
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -119,8 +131,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="U2F\Services" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
<Folder Include="wwwroot\vendor\u2f" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -128,12 +143,24 @@
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndRestServices.cshtml">
|
||||
<Content Update="Views\Home\BitpayTranslator.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LightningChargeServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LightningWalletServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\P2PService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\ShowToken.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Stores\PayButtonEnable.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
@ -143,7 +170,7 @@
|
||||
<Content Update="Views\Public\PayButtonHandle.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndGrpcServices.cshtml">
|
||||
<Content Update="Views\Server\LndServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Maintenance.cshtml">
|
||||
@ -155,9 +182,21 @@
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBT.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletRescan.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletTransactions.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
@ -171,10 +210,4 @@
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -15,7 +15,7 @@ using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -37,16 +37,18 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string LogFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DataDir
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -54,6 +56,22 @@ namespace BTCPayServer.Configuration
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
public bool DisableRegistration
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public static string GetDebugLog(IConfiguration configuration)
|
||||
{
|
||||
return configuration.GetValue<string>("debuglog", null);
|
||||
}
|
||||
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
|
||||
{
|
||||
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
|
||||
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
|
||||
}
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
@ -86,48 +104,57 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
void externalLnd<T>(string code, string lndType)
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>(code, string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
var instanceType = typeof(T);
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
|
||||
}
|
||||
};
|
||||
|
||||
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
|
||||
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
var services = conf.GetOrDefault<string>("externalservices", null);
|
||||
if (services != null)
|
||||
{
|
||||
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(p => p.SeparatorIndex != -1)
|
||||
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
|
||||
Link: p.p.Substring(p.SeparatorIndex + 1))))
|
||||
{
|
||||
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
|
||||
OtherExternalServices.AddOrReplace(service.Name, uri);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||
if(!string.IsNullOrEmpty(socksEndpointString))
|
||||
{
|
||||
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -173,12 +200,20 @@ namespace BTCPayServer.Configuration
|
||||
RootPath = "/" + RootPath;
|
||||
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
if (old != null)
|
||||
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
|
||||
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
|
||||
|
||||
LogFile = GetDebugLog(conf);
|
||||
if (!string.IsNullOrEmpty(LogFile))
|
||||
{
|
||||
Logs.Configuration.LogInformation("LogFile: " + LogFile);
|
||||
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
}
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
var settings = new SSHSettings();
|
||||
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
|
||||
if (settings.Server != null)
|
||||
@ -205,12 +240,6 @@ namespace BTCPayServer.Configuration
|
||||
settings.Username = "root";
|
||||
}
|
||||
}
|
||||
else if (externalUrl != null)
|
||||
{
|
||||
settings.Port = 22;
|
||||
settings.Username = "root";
|
||||
settings.Server = externalUrl.DnsSafeHost;
|
||||
}
|
||||
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
|
||||
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
|
||||
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
|
||||
@ -224,7 +253,9 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -237,11 +268,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
@ -253,14 +279,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
public string TorrcFile { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -37,6 +38,8 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
else if (typeof(T) == typeof(string))
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPAddress))
|
||||
return (T)(object)IPAddress.Parse(str);
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
@ -40,7 +40,11 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
@ -48,6 +52,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
@ -106,6 +112,8 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine("#httpscertificatefilepath=devtest.pfx");
|
||||
builder.AppendLine("#httpscertificatefilepassword=toto");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public abstract class ExternalLnd : ExternalService
|
||||
{
|
||||
public ExternalLnd(LightningConnectionString connectionString, LndTypes type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public LndTypes Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public enum LndTypes
|
||||
{
|
||||
gRPC, Rest
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { }
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalServices : MultiValueDictionary<string, ExternalService>
|
||||
{
|
||||
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
|
||||
{
|
||||
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
|
||||
return Array.Empty<T>();
|
||||
return services.OfType<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
}
|
||||
}
|
200
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
200
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public ExternalConnectionString()
|
||||
{
|
||||
|
||||
}
|
||||
public ExternalConnectionString(Uri server)
|
||||
{
|
||||
Server = server;
|
||||
}
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
81
BTCPayServer/Configuration/ExternalService.cs
Normal file
81
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge,
|
||||
P2P
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
@ -13,7 +14,7 @@ using System.Threading.Tasks;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
[BitpayAPIConstraint()]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -34,20 +35,19 @@ namespace BTCPayServer.Controllers
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new BitpayHttpException(400, "The request body is missing");
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
if (string.IsNullOrEmpty(request.PairingCode))
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required");
|
||||
if (string.IsNullOrEmpty(request.Facade))
|
||||
throw new BitpayHttpException(400, "'facade' property is required");
|
||||
|
||||
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
|
||||
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
|
||||
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = pairingCode,
|
||||
Facade = request.Facade,
|
||||
Label = request.Label
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ namespace BTCPayServer.Controllers
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
Facade = pairingEntity.Facade,
|
||||
Facade = "merchant",
|
||||
Token = pairingEntity.TokenValue,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services.U2F;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using Newtonsoft.Json;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -28,10 +31,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly U2FService _u2FService;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -39,15 +45,21 @@ namespace BTCPayServer.Controllers
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository)
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
U2FService u2FService)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_u2FService = u2FService;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
@ -88,8 +100,44 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
|
||||
{
|
||||
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
LoginWith2faViewModel twoFModel = null;
|
||||
|
||||
if (user.TwoFactorEnabled)
|
||||
{
|
||||
// we need to do an actual sign in attempt so that 2fa can function in next step
|
||||
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
twoFModel = new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
};
|
||||
}
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = twoFModel,
|
||||
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
@ -98,10 +146,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(LoginWith2fa), new
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
returnUrl,
|
||||
model.RememberMe
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel()
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
@ -120,6 +170,71 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
|
||||
{
|
||||
if (_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
|
||||
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
|
||||
|
||||
return new LoginWithU2FViewModel()
|
||||
{
|
||||
Version = u2fChallenge[0].version,
|
||||
Challenge = u2fChallenge[0].challenge,
|
||||
Challenges = JsonConvert.SerializeObject(u2fChallenge),
|
||||
AppId = u2fChallenge[0].appId,
|
||||
UserId = user.Id,
|
||||
RememberMe = rememberMe
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
|
||||
{
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
|
||||
_logger.LogInformation("User logged in.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWithU2FViewModel = viewModel,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
@ -132,10 +247,13 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View(model);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -172,7 +290,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View();
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,12 +393,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
|
||||
if(_Options.DisableRegistration)
|
||||
{
|
||||
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
|
||||
policies.LockSubscription = true;
|
||||
await _SettingsRepository.UpdateSetting(policies);
|
||||
}
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
RegisteredUserId = user.Id;
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
if(logon)
|
||||
@ -436,8 +566,9 @@ namespace BTCPayServer.Controllers
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
|
170
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
170
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public object Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Title = settings.Title,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
PerksTemplate = settings.PerksTemplate,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = app.TagAllInvoices,
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
|
||||
}
|
||||
|
||||
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && !parsedSounds.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
|
||||
}
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var newSettings = new CrowdfundSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate,
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _htmlSanitizer.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
SoundsEnabled = vm.SoundsEnabled,
|
||||
DisqusShortname = vm.DisqusShortname,
|
||||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity,
|
||||
Sounds = parsedSounds,
|
||||
AnimationColors = parsedAnimationColors
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
|
||||
_EventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@ -15,28 +19,68 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "My awesome Point of Sale";
|
||||
Title = "Tea shop";
|
||||
Currency = "USD";
|
||||
Template =
|
||||
"tea:\n" +
|
||||
" price: 0.02\n" +
|
||||
" title: Green Tea # title is optional, defaults to the keys\n\n" +
|
||||
"coffee:\n" +
|
||||
" price: 1\n\n" +
|
||||
"bamba:\n" +
|
||||
" price: 3\n\n" +
|
||||
"beer:\n" +
|
||||
" price: 7\n\n" +
|
||||
"hat:\n" +
|
||||
" price: 15\n\n" +
|
||||
"tshirt:\n" +
|
||||
" price: 25";
|
||||
"green tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Green Tea\n" +
|
||||
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2015/03/26/11/03/green-tea-692339__480.jpg\n\n" +
|
||||
"black tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Black Tea\n" +
|
||||
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2016/11/29/13/04/beverage-1869716__480.jpg\n\n" +
|
||||
"rooibos:\n" +
|
||||
" price: 1.2\n" +
|
||||
" title: Rooibos\n" +
|
||||
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2017/01/08/08/14/water-1962388__480.jpg\n\n" +
|
||||
"pu erh:\n" +
|
||||
" price: 2\n" +
|
||||
" title: Pu Erh\n" +
|
||||
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2018/07/21/16/56/tea-cup-3552917__480.jpg\n\n" +
|
||||
"herbal tea:\n" +
|
||||
" price: 1.8\n" +
|
||||
" title: Herbal Tea\n" +
|
||||
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2015/07/02/20/57/chamomile-829538__480.jpg\n" +
|
||||
" custom: true\n\n" +
|
||||
"fruit tea:\n" +
|
||||
" price: 1.5\n" +
|
||||
" title: Fruit Tea\n" +
|
||||
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
|
||||
" custom: true";
|
||||
EnableShoppingCart = false;
|
||||
ShowCustomAmount = true;
|
||||
ShowDiscount = true;
|
||||
EnableTips = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
|
||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
|
||||
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
||||
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
|
||||
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
|
||||
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
|
||||
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string NotificationEmail { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -47,12 +91,26 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Id = appId,
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template
|
||||
Template = settings.Template,
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
@ -73,7 +131,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
|
||||
var items = _AppService.Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
@ -95,11 +153,11 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
|
||||
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
_AppsHelper.Parse(vm.Template, vm.Currency);
|
||||
_AppService.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -115,9 +173,21 @@ namespace BTCPayServer.Controllers
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
@ -131,8 +201,25 @@ namespace BTCPayServer.Controllers
|
||||
ctx.Apps.Add(app);
|
||||
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
||||
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
|
||||
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,9 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -24,19 +27,31 @@ namespace BTCPayServer.Controllers
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
AppService AppService)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
_currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_AppService = AppService;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private AppService _AppService;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
@ -44,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
var apps = await _AppService.GetAllApps(GetUserId());
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
@ -58,7 +73,7 @@ namespace BTCPayServer.Controllers
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await DeleteApp(appData))
|
||||
if (await _AppService.DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
@ -67,10 +82,15 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppService.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must have created at least one store";
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var vm = new CreateAppViewModel();
|
||||
@ -82,10 +102,15 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppService.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must own at least one store";
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var selectedStore = vm.SelectedStore;
|
||||
@ -117,9 +142,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
CreatedAppId = id;
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.PointOfSale:
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
case AppType.Crowdfund:
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
|
||||
default:
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -139,56 +172,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
{
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DeleteApp(AppData appData)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
private async Task<bool> IsEmailConfigured(string storeId)
|
||||
{
|
||||
return (await (_emailSenderFactory.GetEmailSender(storeId) as EmailSender)?.GetEmailSettings())?.IsComplete() is true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
@ -20,48 +33,203 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
public AppsPublicController(AppService AppService,
|
||||
BTCPayServerOptions btcPayServerOptions,
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_AppService = AppService;
|
||||
_BtcPayServerOptions = btcPayServerOptions;
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private AppService _AppService;
|
||||
private readonly BTCPayServerOptions _BtcPayServerOptions;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
|
||||
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = _AppsHelper.Parse(settings.Template, settings.Currency)
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
|
||||
{
|
||||
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
|
||||
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
|
||||
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
|
||||
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
|
||||
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
|
||||
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
|
||||
},
|
||||
Items = _AppService.Parse(settings.Template, settings.Currency),
|
||||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
CustomTipPercentages = settings.CustomTipPercentages,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
AppId = appId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
|
||||
appInfo.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (settings.Enabled)
|
||||
return View(appInfo);
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(appInfo);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled && !isAdmin)
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
info.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (!isAdmin &&
|
||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
|
||||
(settings.EnforceTargetAmount &&
|
||||
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
|
||||
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var store = await _AppService.GetStore(app);
|
||||
var title = settings.Title;
|
||||
var price = request.Amount;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
if (request.Amount > price)
|
||||
price = request.Amount;
|
||||
}
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
{
|
||||
return NotFound("Contribution Amount is more than is currently allowed.");
|
||||
}
|
||||
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ??
|
||||
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> {AppService.GetAppInternalTag(appId)},
|
||||
cancellationToken: cancellationToken);
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||
new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(invoice.Data.Id);
|
||||
}
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
string choiceKey,
|
||||
string posData = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
@ -69,134 +237,59 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
var choices = _AppService.Parse(settings.Template, settings.Currency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
if (amount > price)
|
||||
price = amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await _AppsHelper.GetStore(app);
|
||||
var store = await _AppService.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken);
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(AppData app)
|
||||
private string GetUserId()
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
return _Currencies.GetCurrencyData(currency, useFallback);
|
||||
}
|
||||
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
|
||||
{
|
||||
if (userId == null || appId == null)
|
||||
return null;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type.Value.ToString() != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
}
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -57,12 +58,12 @@ namespace BTCPayServer.Controllers
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
var response1 = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = response1;
|
||||
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
|
||||
while (true)
|
||||
{
|
||||
if (callCounter > 10)
|
||||
@ -70,13 +71,13 @@ namespace BTCPayServer.Controllers
|
||||
BadRequest();
|
||||
}
|
||||
|
||||
var response2 = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
callCounter++;
|
||||
if (response2 < toCurrencyAmount)
|
||||
if (computedAmount < toCurrencyAmount)
|
||||
{
|
||||
var newCurrentAmount = ((toCurrencyAmount / response2) * 1m) * currentAmount;
|
||||
|
||||
currentAmount = newCurrentAmount;
|
||||
currentAmount =
|
||||
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
|
||||
toCurrencyAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -102,11 +103,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
@ -114,4 +115,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public bool IsTest { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,21 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
private readonly CssThemeManager _cachedServerSettings;
|
||||
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_cachedServerSettings = cachedServerSettings;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
|
||||
{
|
||||
var serviceProvider = HttpContext.RequestServices;
|
||||
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
|
||||
controller.Url = Url;
|
||||
controller.ControllerContext = ControllerContext;
|
||||
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
|
||||
if (res != null)
|
||||
{
|
||||
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
|
||||
return res; // return
|
||||
}
|
||||
}
|
||||
|
||||
return View("Home");
|
||||
}
|
||||
|
||||
[Route("translate")]
|
||||
public IActionResult BitpayTranslator()
|
||||
{
|
||||
return View(new BitpayTranslatorViewModel());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("translate")]
|
||||
public async Task<IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
vm.BitpayLink = vm.BitpayLink ?? string.Empty;
|
||||
vm.BitpayLink = vm.BitpayLink.Trim();
|
||||
if (!vm.BitpayLink.StartsWith("bitcoin:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var invoiceId = vm.BitpayLink.Substring(vm.BitpayLink.LastIndexOf("=", StringComparison.OrdinalIgnoreCase) + 1);
|
||||
vm.BitpayLink = $"bitcoin:?r=https://bitpay.com/i/{invoiceId}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new Exception("This tool only work with bitpay");
|
||||
}
|
||||
|
||||
var client = HttpClientFactory.CreateClient();
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request"));
|
||||
var result = await client.SendAsync(request);
|
||||
// {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"}
|
||||
var str = await result.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
var jobj = JObject.Parse(str);
|
||||
vm.Address = ((JArray)jobj["outputs"])[0]["address"].Value<string>();
|
||||
var amount = Money.Satoshis(((JArray)jobj["outputs"])[0]["amount"].Value<long>());
|
||||
vm.Amount = amount.ToString();
|
||||
vm.BitcoinUri = $"bitcoin:{vm.Address}?amount={amount.ToString()}";
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.BitpayLink), $"Invalid or expired bitpay invoice");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.BitpayLink), $"Error while requesting {ex.Message}");
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
public IActionResult About()
|
||||
{
|
||||
ViewData["Message"] = "Your application description page.";
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
@ -12,7 +13,6 @@ using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
@ -33,23 +33,27 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = id,
|
||||
StoreId = new[] { HttpContext.GetStoreData().Id }
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
@ -64,15 +68,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
Skip = offset,
|
||||
EndDate = dateEnd,
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
OrderId = orderId == null ? null : new[] { orderId },
|
||||
ItemCode = itemCode == null ? null : new[] { itemCode },
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
|
||||
if (store.StoreCertificate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentRequestActionResult(request);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}", Order = 99)]
|
||||
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,25 +2,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -28,26 +34,27 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId(),
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var prodInfo = invoice.ProductInformation;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
var model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
State = invoice.GetInvoiceState().ToString(),
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
@ -58,25 +65,46 @@ namespace BTCPayServer.Controllers
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
||||
StatusMessage = StatusMessage
|
||||
};
|
||||
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
|
||||
var details = InvoicePopulatePayments(invoice);
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.OnChainPayments = details.OnChainPayments;
|
||||
model.OffChainPayments = details.OffChainPayments;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
var model = new InvoiceDetailsModel();
|
||||
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (onchainMethod != null)
|
||||
@ -84,72 +112,46 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
foreach (var payment in invoice.GetPayments())
|
||||
{
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
|
||||
|
||||
int confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(onChainPayments);
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
||||
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
model.OnChainPayments.Add(m);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (LightningLikePaymentData)paymentData;
|
||||
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
});
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
@ -174,17 +176,20 @@ namespace BTCPayServer.Controllers
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null,
|
||||
[FromQuery]string view = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
@ -202,31 +207,46 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
[HttpGet]
|
||||
[Route("invoice-noscript")]
|
||||
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
bool isDefaultPaymentId = false;
|
||||
if (paymentMethodId == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
|
||||
isDefaultCrypto = true;
|
||||
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
isDefaultPaymentId = true;
|
||||
}
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
if (network == null && isDefaultPaymentId)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if (!isDefaultCrypto)
|
||||
if (!isDefaultPaymentId)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
@ -235,7 +255,6 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
@ -251,6 +270,11 @@ namespace BTCPayServer.Controllers
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
: null;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
@ -260,17 +284,18 @@ namespace BTCPayServer.Controllers
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
@ -283,6 +308,7 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
@ -293,12 +319,18 @@ namespace BTCPayServer.Controllers
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
ChangellyEnabled = changelly != null,
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
@ -328,9 +360,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
@ -350,9 +381,12 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
[Route("invoice/{invoiceId}/status")]
|
||||
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
|
||||
[Route("invoice/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
||||
{
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -360,12 +394,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status/ws")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
|
||||
[Route("invoice/{invoiceId}/status/ws")]
|
||||
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
|
||||
[Route("invoice/status/ws")]
|
||||
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -405,6 +443,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
[Route("invoice/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -412,60 +451,95 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
return Ok("{}");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
StatusMessage = StatusMessage,
|
||||
TimezoneOffset = timezoneOffset
|
||||
};
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
|
||||
invoiceQuery.Count = count;
|
||||
invoiceQuery.Skip = skip;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
var list = await ListInvoicesProcess(searchTerm, skip, count);
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
ShowCheckout = invoice.Status == "new",
|
||||
Status = invoice.Status,
|
||||
StatusString = state.ToString(),
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice)
|
||||
});
|
||||
}
|
||||
model.Total = await counting;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
var fs = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
TextSearch = fs.TextSearch,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
});
|
||||
|
||||
return list;
|
||||
Unusual = fs.GetFilterBool("unusual"),
|
||||
Status = fs.GetFilterArray("status"),
|
||||
ExceptionStatus = fs.GetFilterArray("exceptionstatus"),
|
||||
StoreId = fs.GetFilterArray("storeid"),
|
||||
ItemCode = fs.GetFilterArray("itemcode"),
|
||||
OrderId = fs.GetFilterArray("orderid"),
|
||||
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
|
||||
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
var res = model.Process(invoices, format);
|
||||
|
||||
var cd = new ContentDisposition
|
||||
{
|
||||
FileName = $"btcpay-export-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
|
||||
Inline = true
|
||||
};
|
||||
Response.Headers.Add("Content-Disposition", cd.ToString());
|
||||
Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||||
return Content(res, "application/" + format);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
@ -478,17 +552,36 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
return View(new CreateInvoiceModel() { Stores = stores });
|
||||
|
||||
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
|
||||
{
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
|
||||
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
|
||||
nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text));
|
||||
|
||||
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
|
||||
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
|
||||
{
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
|
||||
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
|
||||
nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text));
|
||||
model.AvailablePaymentMethods = paymentMethods;
|
||||
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if (store == null)
|
||||
{
|
||||
@ -511,6 +604,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
@ -521,7 +615,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
@ -533,7 +627,11 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
|
||||
{
|
||||
Enabled = true
|
||||
})
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
@ -546,30 +644,45 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
public async Task<IActionResult> ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId()
|
||||
})).FirstOrDefault();
|
||||
|
||||
var model = new InvoiceStateChangeModel();
|
||||
if (invoice == null)
|
||||
{
|
||||
model.NotFound = true;
|
||||
return NotFound(model);
|
||||
}
|
||||
|
||||
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
model.StatusString = new InvoiceState("invalid", "marked").ToString();
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
model.StatusString = new InvoiceState("complete", "marked").ToString();
|
||||
}
|
||||
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
public class InvoiceStateChangeModel
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
public bool NotFound { get; set; }
|
||||
public string StatusString { get; set; }
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -583,5 +696,49 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
public class PosDataParser
|
||||
{
|
||||
public static Dictionary<string, object> ParsePosData(string posData)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
if (string.IsNullOrEmpty(posData))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jObject = JObject.Parse(posData);
|
||||
foreach (var item in jObject)
|
||||
{
|
||||
|
||||
switch (item.Value.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
var items = item.Value.AsEnumerable().ToList();
|
||||
for (var i = 0; i < items.Count(); i++)
|
||||
{
|
||||
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
|
||||
}
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
result.Add(item.Key, ParsePosData(item.Value.ToString()));
|
||||
break;
|
||||
default:
|
||||
result.Add(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.Add(string.Empty, posData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,21 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
@ -60,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
@ -68,13 +71,12 @@ namespace BTCPayServer.Controllers
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
@ -82,10 +84,18 @@ namespace BTCPayServer.Controllers
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
|
||||
if (invoice.NotificationURL != null &&
|
||||
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
|
||||
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
|
||||
{
|
||||
entity.NotificationURL = notificationUri.AbsoluteUri;
|
||||
}
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
if (additionalTags != null)
|
||||
entity.InternalTags.AddRange(additionalTags);
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -94,17 +104,48 @@ namespace BTCPayServer.Controllers
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
int divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
|
||||
divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
|
||||
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
|
||||
|
||||
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
|
||||
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
|
||||
{
|
||||
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
}
|
||||
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
@ -118,8 +159,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
@ -146,7 +186,7 @@ namespace BTCPayServer.Controllers
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error.ToString());
|
||||
@ -157,9 +197,29 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
|
||||
foreach (var app in await getAppsTaggingStore)
|
||||
{
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
}
|
||||
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await fetchingAll;
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
|
||||
}
|
||||
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
|
||||
});
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
@ -187,6 +247,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
@ -199,10 +260,13 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
|
||||
using (logs.Measure($"{logPrefix} Payment method details creation"))
|
||||
{
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
}
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
@ -230,7 +294,7 @@ namespace BTCPayServer.Controllers
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
logs.Write($"{logPrefix} {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -241,7 +305,7 @@ namespace BTCPayServer.Controllers
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.TxFee = paymentMethod.NextNetworkFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
|
68
BTCPayServer/Controllers/Macaroons.cs
Normal file
68
BTCPayServer/Controllers/Macaroons.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class Macaroons
|
||||
{
|
||||
public class Macaroon
|
||||
{
|
||||
public Macaroon(byte[] bytes)
|
||||
{
|
||||
Bytes = bytes;
|
||||
Hex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(bytes);
|
||||
}
|
||||
|
||||
public string Hex { get; set; }
|
||||
public byte[] Bytes { get; set; }
|
||||
}
|
||||
public static async Task<Macaroons> GetFromDirectoryAsync(string directoryPath)
|
||||
{
|
||||
if (directoryPath == null)
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (Path.GetFileName(file))
|
||||
{
|
||||
case "admin.macaroon":
|
||||
macaroons.AdminMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
case "readonly.macaroon":
|
||||
macaroons.ReadonlyMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
case "invoice.macaroon":
|
||||
macaroons.InvoiceMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
public Macaroon AdminMacaroon { get; set; }
|
||||
}
|
||||
}
|
205
BTCPayServer/Controllers/ManageController.2FA.cs
Normal file
205
BTCPayServer/Controllers/ManageController.2FA.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TwoFactorAuthentication()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new TwoFactorAuthenticationViewModel
|
||||
{
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
|
||||
Is2faEnabled = user.TwoFactorEnabled,
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Disable2faWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disable2fa()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EnableAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
|
||||
.Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ResetAuthenticatorWarning()
|
||||
{
|
||||
return View(nameof(ResetAuthenticator));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
89
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
89
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> U2FAuthentication(string statusMessage = null)
|
||||
{
|
||||
return View(new U2FAuthenticationViewModel()
|
||||
{
|
||||
StatusMessage = statusMessage,
|
||||
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RemoveU2FDevice(string id)
|
||||
{
|
||||
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = "Device removed"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AddU2FDevice(string name)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot register U2F device while not on https or tor"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
|
||||
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
|
||||
|
||||
return View(new AddU2FDeviceViewModel()
|
||||
{
|
||||
AppId = serverRegisterResponse.AppId,
|
||||
Challenge = serverRegisterResponse.Challenge,
|
||||
Version = serverRegisterResponse.Version,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
|
||||
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
|
||||
{
|
||||
StatusMessage = "Device added!"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
@ -9,56 +8,59 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NBitpayClient;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.U2F;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
public partial class ManageController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
IHostingEnvironment _Env;
|
||||
private readonly U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
public ManageController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
ILogger<ManageController> logger,
|
||||
UrlEncoder urlEncoder,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository,
|
||||
IHostingEnvironment env)
|
||||
IHostingEnvironment env,
|
||||
U2FService u2FService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
_Env = env;
|
||||
_u2FService = u2FService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
@ -113,6 +115,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
|
||||
}
|
||||
await _userManager.SetUserNameAsync(user, model.Username);
|
||||
}
|
||||
|
||||
var phoneNumber = user.PhoneNumber;
|
||||
@ -156,8 +159,7 @@ namespace BTCPayServer.Controllers
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
var email = user.Email;
|
||||
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
|
||||
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
|
||||
StatusMessage = "Verification email sent. Please check your email.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
@ -339,163 +341,6 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ExternalLogins));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TwoFactorAuthentication()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new TwoFactorAuthenticationViewModel
|
||||
{
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
|
||||
Is2faEnabled = user.TwoFactorEnabled,
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Disable2faWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disable2fa()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EnableAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError("model.Code", "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ResetAuthenticatorWarning()
|
||||
{
|
||||
return View(nameof(ResetAuthenticator));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
@ -506,33 +351,6 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
378
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
378
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("payment-requests")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public class PaymentRequestController : Controller
|
||||
{
|
||||
private readonly InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public PaymentRequestController(
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
{
|
||||
UserId = GetUserId(), Skip = skip, Count = count
|
||||
});
|
||||
return View(new ListPaymentRequestsViewModel()
|
||||
{
|
||||
Skip = skip,
|
||||
StatusMessage = statusMessage,
|
||||
Count = count,
|
||||
Total = result.Total,
|
||||
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
SelectList stores = null;
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
if (!stores.Any())
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return View(new UpdatePaymentRequestViewModel(data)
|
||||
{
|
||||
Stores = stores,
|
||||
StatusMessage = statusMessage
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(viewModel.Currency) ||
|
||||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
|
||||
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
|
||||
nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
data = new PaymentRequestData();
|
||||
}
|
||||
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||
blob.Currency = viewModel.Currency;
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
|
||||
data.SetBlob(blob);
|
||||
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||
_EventAggregator.Publish(new PaymentRequestUpdated()
|
||||
{
|
||||
Data = data,
|
||||
PaymentRequestId = data.Id
|
||||
});
|
||||
|
||||
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
|
||||
{
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blob = data.GetBlob();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
|
||||
if (result)
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new {StatusMessage = "Payment request successfully removed"});
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage =
|
||||
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
result.StatusMessage = statusMessage;
|
||||
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has already been settled.");
|
||||
}
|
||||
|
||||
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has expired");
|
||||
}
|
||||
|
||||
var statusesAllowedToDisplay = new List<InvoiceStatus>()
|
||||
{
|
||||
InvoiceStatus.New
|
||||
};
|
||||
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
|
||||
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
|
||||
statusesAllowedToDisplay.Contains(status));
|
||||
|
||||
if (validInvoice != null)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
|
||||
}
|
||||
|
||||
return Ok(validInvoice.Id);
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
amount = Math.Min(result.AmountDue, amount.Value);
|
||||
else
|
||||
amount = result.AmountDue;
|
||||
|
||||
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
|
||||
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.Value,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
|
||||
cancellationToken: cancellationToken))
|
||||
.Data.Id;
|
||||
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
|
||||
}
|
||||
|
||||
return Ok(newInvoiceId);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/cancel")]
|
||||
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null )
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = result.Invoices.SingleOrDefault(requestInvoice =>
|
||||
requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New),StringComparison.InvariantCulture) && !requestInvoice.Payments.Any());
|
||||
|
||||
if (invoice == null )
|
||||
{
|
||||
return BadRequest("No unpaid pending invoice to cancel");
|
||||
}
|
||||
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
|
||||
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, InvoiceEvent.MarkedInvalid));
|
||||
|
||||
if (redirect)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPaymentRequest), new
|
||||
{
|
||||
Id = id,
|
||||
StatusMessage = "Payment cancelled"
|
||||
});
|
||||
}
|
||||
|
||||
return Ok("Payment cancelled");
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/clone")]
|
||||
public async Task<IActionResult> ClonePaymentRequest(string id)
|
||||
{
|
||||
var result = await EditPaymentRequest(id);
|
||||
if (result is ViewResult viewResult)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
model.Id = null;
|
||||
model.Title = $"Clone of {model.Title}";
|
||||
|
||||
return View("EditPaymentRequest", model);
|
||||
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
@ -27,7 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
@ -45,7 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
@ -55,7 +57,7 @@ namespace BTCPayServer.Controllers
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
||||
[Route("embed/{storeId}/{cryptoCode}/ln")]
|
||||
[AllowAnonymous]
|
||||
public class PublicLightningNodeInfoController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
|
||||
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
|
||||
{
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_LightningLikePaymentHandler = lightningLikePaymentHandler;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
{
|
||||
Available = true,
|
||||
NodeInfo = nodeInfo.ToString(),
|
||||
CryptoCode = cryptoCode,
|
||||
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network)
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return View(new ShowLightningNodeInfoViewModel() {Available = false, CryptoCode = cryptoCode});
|
||||
}
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
? Url.Content(network.CryptoImagePath)
|
||||
: Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShowLightningNodeInfoViewModel
|
||||
{
|
||||
public string NodeInfo { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
}
|
||||
}
|
@ -12,11 +12,14 @@ using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class RateController : Controller
|
||||
{
|
||||
RateFetcher _RateProviderFactory;
|
||||
@ -43,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
@ -62,7 +65,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
|
||||
|
||||
var result = await GetRates2(currencypairs, store.Id);
|
||||
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -73,10 +76,10 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}/{currency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -86,10 +89,9 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -116,7 +118,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
@ -137,15 +139,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
|
||||
|
||||
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
@ -166,7 +163,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
|
289
BTCPayServer/Controllers/ServerController.Storage.cs
Normal file
289
BTCPayServer/Controllers/ServerController.Storage.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage;
|
||||
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.Models;
|
||||
using BTCPayServer.Storage.ViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ServerController
|
||||
{
|
||||
[HttpGet("server/files/{fileId?}")]
|
||||
public async Task<IActionResult> Files(string fileId = null, string statusMessage = null)
|
||||
{
|
||||
TempData["StatusMessage"] = statusMessage;
|
||||
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId);
|
||||
|
||||
return View(new ViewFilesViewModel()
|
||||
{
|
||||
Files = await _StoredFileRepository.GetFiles(),
|
||||
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
|
||||
DirectFileUrl = fileUrl,
|
||||
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("server/files/{fileId}/delete")]
|
||||
public async Task<IActionResult> DeleteFile(string fileId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _FileService.RemoveFile(fileId, null);
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
fileId = "",
|
||||
statusMessage = "File removed"
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
statusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = e.Message
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("server/files/{fileId}/tmp")]
|
||||
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId)
|
||||
{
|
||||
var file = await _StoredFileRepository.GetFile(fileId);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return View(new CreateTemporaryFileUrlViewModel());
|
||||
}
|
||||
|
||||
[HttpPost("server/files/{fileId}/tmp")]
|
||||
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId,
|
||||
CreateTemporaryFileUrlViewModel viewModel)
|
||||
{
|
||||
if (viewModel.TimeAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var file = await _StoredFileRepository.GetFile(fileId);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var expiry = DateTimeOffset.Now;
|
||||
switch (viewModel.TimeType)
|
||||
{
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds:
|
||||
expiry =expiry.AddSeconds(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes:
|
||||
expiry = expiry.AddMinutes(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Hours:
|
||||
expiry = expiry.AddHours(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Days:
|
||||
expiry = expiry.AddDays(viewModel.TimeAmount);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload);
|
||||
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. <a href='{url}' target='_blank'>{url}</a>"
|
||||
}.ToString(),
|
||||
fileId,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public class CreateTemporaryFileUrlViewModel
|
||||
{
|
||||
public enum TmpFileTimeType
|
||||
{
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
Days
|
||||
}
|
||||
public int TimeAmount { get; set; }
|
||||
public TmpFileTimeType TimeType { get; set; }
|
||||
public bool IsDownload { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("server/files/upload")]
|
||||
public async Task<IActionResult> CreateFile(IFormFile file)
|
||||
{
|
||||
var newFile = await _FileService.AddFile(file, GetUserId());
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
statusMessage = "File added!",
|
||||
fileId = newFile.Id
|
||||
});
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(ControllerContext.HttpContext.User);
|
||||
}
|
||||
|
||||
[HttpGet("server/storage")]
|
||||
public async Task<IActionResult> Storage(bool forceChoice = false, string statusMessage = null)
|
||||
{
|
||||
TempData["StatusMessage"] = statusMessage;
|
||||
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
|
||||
if (forceChoice || savedSettings == null)
|
||||
{
|
||||
return View(new ChooseStorageViewModel()
|
||||
{
|
||||
ShowChangeWarning = savedSettings != null,
|
||||
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(StorageProvider), new
|
||||
{
|
||||
provider = savedSettings.Provider
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("server/storage")]
|
||||
public IActionResult Storage(StorageSettings viewModel)
|
||||
{
|
||||
return RedirectToAction("StorageProvider", "Server", new
|
||||
{
|
||||
provider = viewModel.Provider.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("server/storage/{provider}")]
|
||||
public async Task<IActionResult> StorageProvider(string provider)
|
||||
{
|
||||
if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider))
|
||||
{
|
||||
return RedirectToAction(nameof(Storage), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = $"{provider} provider is not supported"
|
||||
}.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
|
||||
|
||||
var storageProviderService =
|
||||
_StorageProviderServices.SingleOrDefault(service => service.StorageProvider().Equals(storageProvider));
|
||||
|
||||
switch (storageProviderService)
|
||||
{
|
||||
case null:
|
||||
return RedirectToAction(nameof(Storage), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = $"{storageProvider} is not supported"
|
||||
}.ToString()
|
||||
});
|
||||
case AzureBlobStorageFileProviderService fileProviderService:
|
||||
return View(nameof(EditAzureBlobStorageStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case AmazonS3FileProviderService fileProviderService:
|
||||
return View(nameof(EditAmazonS3StorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case GoogleCloudStorageFileProviderService fileProviderService:
|
||||
return View(nameof(EditGoogleCloudStorageStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case FileSystemFileProviderService fileProviderService:
|
||||
return View(nameof(EditFileSystemStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("server/storage/AzureBlobStorage")]
|
||||
public async Task<IActionResult> EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/AmazonS3")]
|
||||
public async Task<IActionResult> EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/GoogleCloudStorage")]
|
||||
public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
|
||||
GoogleCloudStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/FileSystem")]
|
||||
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> SaveStorageProvider(IBaseStorageConfiguration viewModel,
|
||||
StorageProvider storageProvider)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
|
||||
data.Provider = storageProvider;
|
||||
data.Configuration = JObject.FromObject(viewModel);
|
||||
await _SettingsRepository.UpdateSetting(data);
|
||||
TempData["StatusMessage"] = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Storage settings updated successfully"
|
||||
}.ToString();
|
||||
return View(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validations;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@ -25,12 +26,18 @@ using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using BTCPayServer.Storage.Services.Providers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
public partial class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
@ -38,23 +45,40 @@ namespace BTCPayServer.Controllers
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
private readonly TorServices _torServices;
|
||||
BTCPayServerOptions _Options;
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly StoredFileRepository _StoredFileRepository;
|
||||
private readonly FileService _FileService;
|
||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
IEnumerable<IStorageProviderService> storageProviderServices,
|
||||
BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
TorServices torServices,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_Options = options;
|
||||
_StoredFileRepository = storedFileRepository;
|
||||
_FileService = fileService;
|
||||
_StorageProviderServices = storageProviderServices;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -167,6 +191,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.DNSDomain = null;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
|
||||
@ -204,8 +229,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
builder.Scheme = this.Request.Scheme;
|
||||
builder.Host = vm.DNSDomain;
|
||||
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
|
||||
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
|
||||
var addresses1 = GetAddressAsync(this.Request.Host.Host);
|
||||
var addresses2 = GetAddressAsync(vm.DNSDomain);
|
||||
await Task.WhenAll(addresses1, addresses2);
|
||||
|
||||
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
|
||||
@ -242,6 +267,13 @@ namespace BTCPayServer.Controllers
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else if (command == "clean")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-clean.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The old docker images will be cleaned soon...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
@ -249,6 +281,13 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(Maintenance));
|
||||
}
|
||||
|
||||
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
|
||||
{
|
||||
if (IPAddress.TryParse(domainOrIP, out var ip))
|
||||
return Task.FromResult(new[] { ip });
|
||||
return Dns.GetHostAddressesAsync(domainOrIP);
|
||||
}
|
||||
|
||||
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
|
||||
[HttpGet]
|
||||
[Route("runid")]
|
||||
@ -341,22 +380,27 @@ namespace BTCPayServer.Controllers
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var isAdmin = IsAdmin(roles);
|
||||
bool updated = false;
|
||||
|
||||
if (isAdmin != viewModel.IsAdmin)
|
||||
viewModel.StatusMessage = "";
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (!viewModel.IsAdmin && admins.Count == 1)
|
||||
{
|
||||
viewModel.StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added.";
|
||||
return View(viewModel); // return
|
||||
}
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (viewModel.IsAdmin != IsAdmin(roles))
|
||||
{
|
||||
if (viewModel.IsAdmin)
|
||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
else
|
||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||
updated = true;
|
||||
}
|
||||
if (updated)
|
||||
{
|
||||
|
||||
viewModel.StatusMessage = "User successfully updated";
|
||||
}
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
@ -367,12 +411,28 @@ namespace BTCPayServer.Controllers
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (IsAdmin(roles))
|
||||
{
|
||||
Title = "Delete user " + user.Email,
|
||||
Description = "This user will be permanently deleted",
|
||||
Action = "Delete"
|
||||
});
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||
"Delete"));
|
||||
}
|
||||
else
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
|
||||
"This user will be permanently deleted",
|
||||
"Delete"));
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
@ -393,91 +453,245 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
|
||||
// load display app dropdown
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userId = _UserManager.GetUserId(base.User);
|
||||
var selectList = ctx.Users.Where(user => user.Id == userId)
|
||||
.SelectMany(s => s.UserStores)
|
||||
.Select(s => s.StoreData)
|
||||
.SelectMany(s => s.Apps)
|
||||
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
|
||||
selectList.Insert(0, new SelectListItem("(None)", null));
|
||||
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
|
||||
}
|
||||
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
|
||||
if (app != null)
|
||||
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
|
||||
else
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not preserved on client side, but clearing it just in case
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public IActionResult Services()
|
||||
public async Task<IActionResult> Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
result.ExternalServices = _Options.ExternalServices.ToList();
|
||||
foreach (var externalService in _Options.OtherExternalServices)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (_Options.SSHSettings != null)
|
||||
{
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
foreach (var torService in _torServices.Services)
|
||||
{
|
||||
if (torService.VirtualPort == 80)
|
||||
{
|
||||
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
Index = i++,
|
||||
Name = torService.Name,
|
||||
Link = $"http://{torService.OnionHost}"
|
||||
});
|
||||
}
|
||||
else if (TryParseAsExternalService(torService, out var externalService))
|
||||
{
|
||||
result.ExternalServices.Add(externalService);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
|
||||
});
|
||||
}
|
||||
}
|
||||
result.HasSSH = _Options.SSHSettings != null;
|
||||
|
||||
var storageSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
|
||||
result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(),
|
||||
Link = Url.Action("Storage")
|
||||
});
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce)
|
||||
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
|
||||
{
|
||||
externalService = null;
|
||||
if (torService.ServiceType == TorServiceType.P2P)
|
||||
{
|
||||
externalService = new ExternalService()
|
||||
{
|
||||
CryptoCode = torService.Network.CryptoCode,
|
||||
DisplayName = "Full node P2P",
|
||||
Type = ExternalServiceTypes.P2P,
|
||||
ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)),
|
||||
ServiceName = torService.Name,
|
||||
};
|
||||
}
|
||||
return externalService != null;
|
||||
}
|
||||
|
||||
private ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
var result = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (result != null)
|
||||
return result;
|
||||
_torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result));
|
||||
return result;
|
||||
}
|
||||
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
var service = GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
if (external.CertificateThumbprint != null)
|
||||
try
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
if (service.Type == ExternalServiceTypes.P2P)
|
||||
{
|
||||
return View("P2PService", new LightningWalletServices()
|
||||
{
|
||||
ShowQR = showQR,
|
||||
WalletName = service.ServiceName,
|
||||
ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
|
||||
});
|
||||
}
|
||||
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
switch (service.Type)
|
||||
{
|
||||
case ExternalServiceTypes.Charge:
|
||||
return LightningChargeServices(service, connectionString, showQR);
|
||||
case ExternalServiceTypes.RTL:
|
||||
case ExternalServiceTypes.Spark:
|
||||
if (connectionString.AccessKey == null)
|
||||
{
|
||||
StatusMessage = $"Error: The access key of the service is not set";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||
{
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||
vm.APIToken = connectionString.APIToken;
|
||||
var builder = new UriBuilder(connectionString.Server);
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||
{
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
|
||||
model.SSL = connectionString.Server.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (connectionString.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = connectionString.CertificateThumbprint;
|
||||
}
|
||||
if (connectionString.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
}
|
||||
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return View(nameof(LndServices), model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
@ -490,73 +704,62 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
[HttpPost]
|
||||
public IActionResult LndGrpcServicesPost(string cryptoCode, int index)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.ChainType = _Options.NetworkType.ToString();
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-rest/{cryptoCode}/{index}")]
|
||||
public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce)
|
||||
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
var service = GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
var model = new LndRestServicesViewModel();
|
||||
|
||||
model.BaseApiUrl = external.BaseUri.ToString();
|
||||
if (external.CertificateThumbprint != null)
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
if (external.Macaroon != null)
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
ExternalConnectionString connectionString = null;
|
||||
try
|
||||
{
|
||||
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
|
||||
return View(model);
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = connectionString.Server.DnsSafeHost;
|
||||
grpcConf.Port = connectionString.Server.Port;
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
|
||||
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
@ -571,9 +774,11 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
|
||||
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
@ -595,19 +800,28 @@ namespace BTCPayServer.Controllers
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
var client = model.Settings.CreateSmtpClient();
|
||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
@ -625,5 +839,67 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/logs/{file?}")]
|
||||
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
var vm = new LogsViewModel();
|
||||
|
||||
if (string.IsNullOrEmpty(_Options.LogFile))
|
||||
{
|
||||
vm.StatusMessage = "Error: File Logging Option not specified. " +
|
||||
"You need to set debuglog and optionally " +
|
||||
"debugloglevel in the configuration or through runtime arguments";
|
||||
}
|
||||
else
|
||||
{
|
||||
var di = Directory.GetParent(_Options.LogFile);
|
||||
if (di == null)
|
||||
{
|
||||
vm.StatusMessage = "Error: Could not load log files";
|
||||
}
|
||||
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
|
||||
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
|
||||
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
|
||||
vm.LogFileCount = logFiles.Length;
|
||||
vm.LogFiles = logFiles
|
||||
.OrderBy(info => info.LastWriteTime)
|
||||
.Skip(offset)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
vm.LogFileOffset = offset;
|
||||
|
||||
if (string.IsNullOrEmpty(file))
|
||||
return View("Logs", vm);
|
||||
vm.Log = "";
|
||||
var path = Path.Combine(di.FullName, file);
|
||||
try
|
||||
{
|
||||
using (var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
vm.Log = await reader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
return View("Logs", vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
BTCPayServer/Controllers/StorageController.cs
Normal file
24
BTCPayServer/Controllers/StorageController.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Storage
|
||||
{
|
||||
[Route("Storage")]
|
||||
public class StorageController
|
||||
{
|
||||
private readonly FileService _FileService;
|
||||
|
||||
public StorageController(FileService fileService)
|
||||
{
|
||||
_FileService = fileService;
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}")]
|
||||
public async Task<IActionResult> GetFile(string fileId)
|
||||
{
|
||||
var url = await _FileService.GetFileUrl(fileId);
|
||||
return new RedirectResult(url);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -34,33 +37,115 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
class GetXPubs
|
||||
{
|
||||
public BitcoinExtPubKey ExtPubKey { get; set; }
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
public HDFingerprint RootFingerprint { get; set; }
|
||||
public string Source { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
|
||||
public async Task<IActionResult> AddDerivationSchemeLedger(
|
||||
string storeId,
|
||||
string cryptoCode,
|
||||
string command,
|
||||
string keyPath = "")
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
|
||||
using (var normalOperationTimeout = new CancellationTokenSource())
|
||||
{
|
||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||
try
|
||||
{
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var k = KeyPath.Parse(keyPath);
|
||||
if (k.Indexes.Length == 0)
|
||||
throw new FormatException("Invalid key path");
|
||||
|
||||
var getxpubResult = new GetXPubs();
|
||||
getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = segwit,
|
||||
Legacy = !segwit
|
||||
});
|
||||
getxpubResult.DerivationScheme = derivation;
|
||||
getxpubResult.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).ExtPubKey.PubKey.GetHDFingerPrint();
|
||||
getxpubResult.Source = hw.Device;
|
||||
result = getxpubResult;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
finally { hw.Dispose(); }
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
|
||||
if (derivation != null)
|
||||
{
|
||||
vm.DerivationScheme = derivation.AccountDerivation.ToString();
|
||||
vm.Config = derivation.ToJson();
|
||||
}
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm,
|
||||
string cryptoCode)
|
||||
{
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -71,52 +156,100 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
DerivationSchemeSettings strategy = null;
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(c => c.DerivationStrategyBase.ToString())
|
||||
.FirstOrDefault();
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
if (!string.IsNullOrEmpty(vm.Config))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Config file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
if (vm.ColdcardPublicFile != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy))
|
||||
{
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Coldcard public file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
strategy = newStrategy;
|
||||
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strategy = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var oldConfig = vm.Config;
|
||||
vm.Config = strategy == null ? null : strategy.ToJson();
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is clicking on continue after changing the config
|
||||
(!vm.Confirmation && oldConfig != vm.Config) ||
|
||||
// - The user is clickingon continue without changing config nor enabling/disabling
|
||||
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
|
||||
|
||||
showAddress = showAddress && strategy != null;
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
await wallet.TrackAsync(strategy.AccountDerivation);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
@ -128,8 +261,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
if (oldConfig != vm.Config)
|
||||
StatusMessage = $"Derivation settings for {network.CryptoCode} has been modified.";
|
||||
if (willBeExcluded != wasExcluded)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
StatusMessage = $"On-Chain payments for {network.CryptoCode} has been {label}.";
|
||||
}
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
@ -146,27 +285,43 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
|
||||
{
|
||||
strategy.AccountDerivation = newStrategy.AccountDerivation;
|
||||
strategy.AccountOriginal = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
vm.StatusMessage =
|
||||
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
using (var stream = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
return await stream.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
var line = strategy.AccountDerivation.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -77,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
|
74
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
74
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public IActionResult UpdateCoinSwitchSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().CoinSwitchSettings;
|
||||
if (existing == null) return;
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.Mode = existing.Mode;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var coinSwitchSettings = new CoinSwitchSettings()
|
||||
{
|
||||
MerchantId = vm.MerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
Mode = vm.Mode,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.CoinSwitchSettings = coinSwitchSettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "CoinSwitch settings modified";
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
BTCPayServer/Controllers/StoresController.Email.cs
Normal file
65
BTCPayServer/Controllers/StoresController.Email.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
|
||||
[Route("{storeId}/emails")]
|
||||
public IActionResult Emails()
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("{storeId}/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
var client = model.Settings.CreateSmtpClient();
|
||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
model.StatusMessage = "Error: " + ex.Message;
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
else // if(command == "Save")
|
||||
{
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.EmailSettings = model.Settings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Email settings modified";
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,8 @@ namespace BTCPayServer.Controllers
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -10,7 +11,9 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -23,6 +26,7 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
@ -51,6 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
ChangellyClientProvider changellyClientProvider,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
@ -59,6 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
MvcJsonOptions = mvcJsonOptions;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
@ -91,6 +97,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[TempData]
|
||||
public bool StoreNotConfigured
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
@ -162,7 +173,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove access to {user.Email}?",
|
||||
Description = $"Are you sure you want to remove store access for {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -178,24 +189,39 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
public IActionResult Rates(string storeId)
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = storeId;
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[] currencyPairs = null;
|
||||
try
|
||||
{
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -209,7 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
@ -257,7 +283,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(currencyPair);
|
||||
}
|
||||
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
|
||||
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||
foreach (var fetch in fetchs)
|
||||
{
|
||||
@ -321,16 +347,35 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
SetCryptoCurrencies(vm, StoreData);
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
return View(vm);
|
||||
}
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
{
|
||||
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
|
||||
.Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray();
|
||||
|
||||
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
|
||||
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
|
||||
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.DefaultPaymentMethod = chosen?.Value;
|
||||
}
|
||||
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
display : $"{display} (Lightning)";
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
@ -355,25 +400,28 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
bool needUpdate = false;
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
|
||||
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
|
||||
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
|
||||
{
|
||||
needUpdate = true;
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
SetCryptoCurrencies(model, StoreData);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
@ -403,7 +451,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
|
||||
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
@ -422,7 +470,7 @@ namespace BTCPayServer.Controllers
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.ToDictionary(c => c.Network.CryptoCode);
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
@ -430,7 +478,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||
Value = strategy?.ToPrettyString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
|
||||
});
|
||||
@ -461,6 +509,14 @@ namespace BTCPayServer.Controllers
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = coinSwitchEnabled,
|
||||
Action = nameof(UpdateCoinSwitchSettings),
|
||||
Provider = "CoinSwitch"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -486,7 +542,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
@ -540,11 +596,11 @@ namespace BTCPayServer.Controllers
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -554,9 +610,9 @@ namespace BTCPayServer.Controllers
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
|
||||
model.StatusMessage = StatusMessage;
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Facade = t.Facade,
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
@ -570,6 +626,45 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}/revoke")]
|
||||
public async Task<IActionResult> RevokeToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null || token.StoreId != StoreData.Id)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Revoke the token",
|
||||
Title = "Revoke the token",
|
||||
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
|
||||
ButtonClass = "btn-danger"
|
||||
});
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/{tokenId}/revoke")]
|
||||
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
StatusMessage = "Token revoked";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}")]
|
||||
public async Task<IActionResult> ShowToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null || token.StoreId != StoreData.Id)
|
||||
return NotFound();
|
||||
return View(token);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
@ -602,7 +697,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
{
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
|
||||
};
|
||||
@ -614,7 +708,6 @@ namespace BTCPayServer.Controllers
|
||||
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = tokenRequest.PairingCode,
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
});
|
||||
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
|
||||
@ -634,6 +727,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
public string GeneratedPairingCode { get; set; }
|
||||
public IOptions<MvcJsonOptions> MvcJsonOptions { get; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
@ -653,7 +747,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
@ -671,21 +764,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/Tokens/Delete")]
|
||||
public async Task<IActionResult> DeleteToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
StatusMessage = "Token revoked";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey()
|
||||
@ -720,7 +798,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Facade = pairing.Facade,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
@ -756,12 +833,17 @@ namespace BTCPayServer.Controllers
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(p => !excludeFilter.Match(p.PaymentId))
|
||||
.Count() == 0;
|
||||
StatusMessage = "Pairing is successful";
|
||||
if (pairingResult == PairingResult.Partial)
|
||||
StatusMessage = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
storeId = store.Id,
|
||||
pairingCode = pairingCode
|
||||
});
|
||||
}
|
||||
else
|
||||
@ -806,7 +888,11 @@ namespace BTCPayServer.Controllers
|
||||
ButtonSize = 2,
|
||||
UrlRoot = appUrl,
|
||||
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
|
||||
StoreId = store.Id
|
||||
StoreId = store.Id,
|
||||
ButtonType = 0,
|
||||
Min = 1,
|
||||
Max = 20,
|
||||
Step = 1
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class WalletsController
|
||||
{
|
||||
|
||||
[NonAction]
|
||||
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
if (network.SupportRBF)
|
||||
{
|
||||
psbtRequest.RBF = !sendModel.DisableRBF;
|
||||
}
|
||||
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(sendModel.Amount.Value);
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
}
|
||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
|
||||
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
|
||||
if (psbt == null)
|
||||
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||
return psbt;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt")]
|
||||
public IActionResult WalletPSBT()
|
||||
{
|
||||
return View(new WalletPSBTViewModel());
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
public async Task<IActionResult> WalletPSBT(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
vm.Decoded = psbt.ToString();
|
||||
return View(vm);
|
||||
}
|
||||
else if (command == "ledger")
|
||||
{
|
||||
return ViewWalletSendLedger(psbt);
|
||||
}
|
||||
else if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
return ViewPSBT(psbt, errors);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "combine")
|
||||
{
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
|
||||
return FilePSBT(psbt, "psbt-export.psbt");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public IActionResult WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string psbt = null)
|
||||
{
|
||||
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
|
||||
{
|
||||
PSBT psbt = null;
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.AddRange(errors.Select(e => e.ToString()));
|
||||
return View(vm);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Error while broadcasting: " + ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return ViewPSBT(psbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Unknown command");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
|
||||
{
|
||||
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
|
||||
}
|
||||
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
|
||||
{
|
||||
return View(nameof(WalletPSBT), new WalletPSBTViewModel()
|
||||
{
|
||||
Decoded = psbt.ToString(),
|
||||
PSBT = psbt.ToBase64(),
|
||||
Errors = errors?.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private IActionResult FilePSBT(PSBT psbt, string fileName)
|
||||
{
|
||||
return File(psbt.ToBytes(), "application/octet-stream", fileName);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/combine")]
|
||||
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
StatusMessage = "PSBT Successfully combined!";
|
||||
return ViewPSBT(sourcePSBT);
|
||||
}
|
||||
}
|
||||
}
|
@ -46,6 +46,9 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
public RateFetcher RateFetcher { get; }
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
CurrencyNameTable _currencyTable;
|
||||
public WalletsController(StoreRepository repo,
|
||||
CurrencyNameTable currencyTable,
|
||||
@ -77,9 +80,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
DerivationStrategy: d.AccountDerivation,
|
||||
Network: d.Network)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(_ => (Wallet: _.Wallet,
|
||||
@ -114,13 +117,12 @@ namespace BTCPayServer.Controllers
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
@ -132,6 +134,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Timestamp = tx.Timestamp;
|
||||
vm.Positive = tx.BalanceChange >= Money.Zero;
|
||||
vm.Balance = tx.BalanceChange.ToString();
|
||||
vm.IsConfirmed = tx.Confirmations != 0;
|
||||
}
|
||||
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
|
||||
return View(model);
|
||||
@ -147,28 +150,37 @@ namespace BTCPayServer.Controllers
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var storeData = store.GetStoreBlob();
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
|
||||
rateRules.Spread = 0.0m;
|
||||
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
|
||||
WalletModel model = new WalletModel()
|
||||
WalletSendModel model = new WalletSendModel()
|
||||
{
|
||||
DefaultAddress = defaultDestination,
|
||||
DefaultAmount = defaultAmount,
|
||||
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
|
||||
CryptoCurrency = walletId.CryptoCode
|
||||
Destination = defaultDestination,
|
||||
CryptoCode = walletId.CryptoCode
|
||||
};
|
||||
if (double.TryParse(defaultAmount, out var amount))
|
||||
model.Amount = (decimal)amount;
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
@ -185,6 +197,101 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
|
||||
if (destination == null)
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
|
||||
|
||||
if (vm.Amount.HasValue)
|
||||
{
|
||||
if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
|
||||
ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
|
||||
if (vm.CurrentBalance < vm.Amount.Value)
|
||||
ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||
var psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
|
||||
|
||||
if (command == "ledger")
|
||||
{
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (command == "analyze-psbt")
|
||||
return ViewPSBT(psbt.PSBT);
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||
{
|
||||
return View("WalletSendLedger", new WalletSendLedgerModel()
|
||||
{
|
||||
PSBT = psbt.ToBase64(),
|
||||
HintChange = hintChange?.ToString(),
|
||||
WebsocketPath = this.Url.Action(nameof(LedgerConnection)),
|
||||
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
||||
});
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination = destination?.Trim();
|
||||
return new IDestination[] { BitcoinAddress.Create(destination, network) };
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (transaction != null)
|
||||
{
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var derivationSettings = await GetDerivationSchemeSettings(walletId);
|
||||
wallet.InvalidateCache(derivationSettings.AccountDerivation);
|
||||
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})";
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
@ -193,18 +300,17 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new RescanWalletModel();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
|
||||
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
|
||||
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
|
||||
if(scanProgress != null)
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
|
||||
if (scanProgress != null)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
|
||||
@ -237,14 +343,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
@ -266,18 +371,24 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
|
||||
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId, StoreData store)
|
||||
{
|
||||
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private async Task<DerivationSchemeSettings> GetDerivationSchemeSettings(WalletId walletId)
|
||||
{
|
||||
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
return GetDerivationSchemeSettings(walletId, store);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
@ -298,218 +409,101 @@ namespace BTCPayServer.Controllers
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
|
||||
[Route("{walletId}/send/ledger/ws")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
|
||||
DerivationStrategyBase derivationScheme = null,
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
string psbt = null,
|
||||
string hintChange = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationSettings = GetDerivationSchemeSettings(walletId, storeData);
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
using (var normalOperationTimeout = new CancellationTokenSource())
|
||||
using (var signTimeout = new CancellationTokenSource())
|
||||
{
|
||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
var model = new WalletSendLedgerModel();
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
||||
result = getxpubResult;
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
|
||||
var changeAddress = await change;
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||
if (derivationSettings.AccountKeyPath == null)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
||||
derivationSettings.AccountDerivation,
|
||||
normalOperationTimeout.Token);
|
||||
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
// If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var c in unspentCoins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||
// but some deployment does not have it, so let's use AccountKeyPath instead
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
|
||||
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
|
||||
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
// We have the root fingerprint, we can check the root from it
|
||||
else
|
||||
{
|
||||
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
}
|
||||
|
||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
|
||||
var psbtResponse = new CreatePSBTResponse()
|
||||
{
|
||||
PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork),
|
||||
ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork)
|
||||
};
|
||||
|
||||
|
||||
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
||||
|
||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(derivationScheme);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@ -534,27 +528,16 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new Exception("The derivation scheme is not provided");
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
return directStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
[JsonProperty("psbt")]
|
||||
public string PSBT { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool TagAllInvoices { get; set; }
|
||||
public string Settings { get; set; }
|
||||
|
||||
public T GetSettings<T>() where T : class, new()
|
||||
|
@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -54,6 +53,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
@ -87,10 +91,18 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoredFile> Files
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -204,6 +216,16 @@ namespace BTCPayServer.Data
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PaymentRequests)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasIndex(o => o.Status);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,9 +3,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
@ -99,15 +96,5 @@ namespace BTCPayServer.Data
|
||||
else if (_Type == DatabaseType.MySQL)
|
||||
builder.UseMySql(_ConnectionString);
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
{
|
||||
builder.UseMemoryStorage();
|
||||
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||
//if (_Type == DatabaseType.Sqlite)
|
||||
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||
//else if (_Type == DatabaseType.Postgres)
|
||||
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,5 +81,10 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
|
||||
public Services.Invoices.InvoiceState GetInvoiceState()
|
||||
{
|
||||
return new Services.Invoices.InvoiceState(Status, ExceptionStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,6 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Unused")]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
|
@ -18,8 +18,11 @@ using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -39,6 +42,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
@ -54,7 +62,6 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
@ -66,7 +73,7 @@ namespace BTCPayServer.Data
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
yield return DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +98,11 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public void SetSupportedPaymentMethod(ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
SetSupportedPaymentMethod(null, supportedPaymentMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set or remove a new supported payment method for the store
|
||||
/// </summary>
|
||||
@ -98,8 +110,16 @@ namespace BTCPayServer.Data
|
||||
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
|
||||
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
throw new InvalidOperationException("Argument mismatch");
|
||||
if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
{
|
||||
throw new InvalidOperationException("Incoherent arguments, this should never happen");
|
||||
}
|
||||
if (supportedPaymentMethod == null && paymentMethodId == null)
|
||||
throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified");
|
||||
if (supportedPaymentMethod != null && paymentMethodId == null)
|
||||
{
|
||||
paymentMethodId = supportedPaymentMethod.PaymentId;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
@ -127,7 +147,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -193,7 +213,7 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
@ -202,13 +222,32 @@ namespace BTCPayServer.Data
|
||||
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
|
||||
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networks)
|
||||
{
|
||||
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
|
||||
PaymentMethodId[] paymentMethodIds = GetEnabledPaymentIds(networks);
|
||||
|
||||
var defaultPaymentId = string.IsNullOrEmpty(DefaultCrypto) ? null : PaymentMethodId.Parse(DefaultCrypto);
|
||||
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ??
|
||||
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
|
||||
paymentMethodIds.FirstOrDefault();
|
||||
return chosen;
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
|
||||
public PaymentMethodId[] GetEnabledPaymentIds(BTCPayNetworkProvider networks)
|
||||
{
|
||||
DefaultCrypto = defaultCryptoCurrency;
|
||||
var excludeFilter = GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var paymentMethodIds = GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
|
||||
.Where(a => !excludeFilter.Match(a))
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC")
|
||||
.ThenBy(a => a.CryptoCode)
|
||||
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
|
||||
.ToArray();
|
||||
return paymentMethodIds;
|
||||
}
|
||||
|
||||
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
|
||||
{
|
||||
DefaultCrypto = defaultPaymentId.ToString();
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
@ -249,6 +288,12 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
@ -258,12 +303,42 @@ namespace BTCPayServer.Data
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
|
||||
[Obsolete("Use NetworkFeeMode instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool? NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public NetworkFeeMode NetworkFeeMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
CurrencyPair[] _DefaultCurrencyPairs;
|
||||
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
|
||||
public CurrencyPair[] DefaultCurrencyPairs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
|
||||
}
|
||||
set
|
||||
{
|
||||
_DefaultCurrencyPairs = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDefaultCurrencyPairString()
|
||||
{
|
||||
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
|
||||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
@ -287,10 +362,11 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
@ -305,6 +381,7 @@ namespace BTCPayServer.Data
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
public CoinSwitchSettings CoinSwitchSettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
@ -367,6 +444,12 @@ namespace BTCPayServer.Data
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
|
||||
[Obsolete("Use DerivationSchemeSettings instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; }
|
||||
|
||||
public EmailSettings EmailSettings { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
@ -12,14 +12,51 @@ namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public BTCPayNetwork BtcPayNetwork { get; }
|
||||
|
||||
public Network Network => BtcPayNetwork.NBitcoinNetwork;
|
||||
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
Dictionary<uint, string[]> ElectrumMapping = new Dictionary<uint, string[]>();
|
||||
|
||||
public DerivationSchemeParser(BTCPayNetwork expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
if (expectedNetwork == null)
|
||||
throw new ArgumentNullException(nameof(expectedNetwork));
|
||||
BtcPayNetwork = expectedNetwork;
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase ParseElectrum(string str)
|
||||
{
|
||||
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(str);
|
||||
if (data.Length < 4)
|
||||
throw new FormatException();
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network);
|
||||
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
throw new FormatException();
|
||||
}
|
||||
if (type == DerivationType.Segwit)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
|
||||
if (type == DerivationType.Legacy)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
|
||||
if (type == DerivationType.SegwitP2SH)
|
||||
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
@ -41,7 +78,7 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
if(!Network.Consensus.SupportSegwit)
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
|
||||
try
|
||||
@ -53,20 +90,19 @@ namespace BTCPayServer
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
bool hasLabel = false;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (IsLabel(parts[i]))
|
||||
{
|
||||
if (!hasLabel)
|
||||
{
|
||||
hintedLabels.Clear();
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
}
|
||||
hasLabel = true;
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
@ -76,17 +112,22 @@ namespace BTCPayServer
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString();
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
foreach (var label in labels)
|
||||
switch (type)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
case DerivationType.Legacy:
|
||||
hintedLabels.Add("legacy");
|
||||
break;
|
||||
case DerivationType.SegwitP2SH:
|
||||
hintedLabels.Add("p2sh");
|
||||
break;
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
@ -128,7 +169,7 @@ namespace BTCPayServer
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
@ -141,20 +182,20 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeSettings : ISupportedPaymentMethod
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
strategy = null;
|
||||
try
|
||||
{
|
||||
strategy = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(config);
|
||||
strategy.Network = network;
|
||||
}
|
||||
catch { }
|
||||
return strategy != null;
|
||||
}
|
||||
|
||||
public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||
{
|
||||
settings = null;
|
||||
if (coldcardExport == null)
|
||||
throw new ArgumentNullException(nameof(coldcardExport));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Source = "Coldcard";
|
||||
var derivationSchemeParser = new DerivationSchemeParser(network);
|
||||
JObject jobj = null;
|
||||
try
|
||||
{
|
||||
jobj = JObject.Parse(coldcardExport);
|
||||
jobj = (JObject)jobj["keystore"];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("xpub"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("label"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = jobj["label"].Value<string>();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("ckcc_xfp"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("derivation"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
settings = result;
|
||||
settings.Network = network;
|
||||
return true;
|
||||
}
|
||||
|
||||
public DerivationSchemeSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
AccountDerivation = derivationStrategy;
|
||||
Network = network;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Source { get; set; }
|
||||
public KeyPath AccountKeyPath { get; set; }
|
||||
|
||||
public DerivationStrategyBase AccountDerivation { get; set; }
|
||||
public string AccountOriginal { get; set; }
|
||||
|
||||
public HDFingerprint? RootFingerprint { get; set; }
|
||||
|
||||
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public BitcoinExtPubKey AccountKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return ExplicitAccountKey ?? new BitcoinExtPubKey(AccountDerivation.GetExtPubKeys().First(), Network.NBitcoinNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
||||
{
|
||||
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
|
||||
{
|
||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||
{
|
||||
AccountKey = AccountKey,
|
||||
AccountKeyPath = AccountKeyPath,
|
||||
MasterFingerprint = fp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return AccountDerivation.ToString();
|
||||
}
|
||||
public string ToPrettyString()
|
||||
{
|
||||
return !string.IsNullOrEmpty(Label) ? Label :
|
||||
!String.IsNullOrEmpty(AccountOriginal) ? AccountOriginal :
|
||||
ToString();
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return Network.NBXplorerNetwork.Serializer.ToString(this);
|
||||
}
|
||||
|
||||
public void RebaseKeyPaths(PSBT psbt)
|
||||
{
|
||||
foreach (var rebase in GetPSBTRebaseKeyRules())
|
||||
{
|
||||
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy : ISupportedPaymentMethod
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
}
|
||||
|
||||
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationStrategy(result, network);
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _DerivationStrategy.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -11,23 +11,14 @@ namespace BTCPayServer.Events
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
State = invoice.GetInvoiceState();
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
public string InvoiceId { get; }
|
||||
public InvoiceState State { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
return $"Invoice status is {State}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,31 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
public const string Created = "invoice_created";
|
||||
public const string ReceivedPayment = "invoice_receivedPayment";
|
||||
public const string MarkedCompleted = "invoice_markedComplete";
|
||||
public const string MarkedInvalid= "invoice_markedInvalid";
|
||||
public const string Expired= "invoice_expired";
|
||||
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
|
||||
public const string PaidInFull= "invoice_paidInFull";
|
||||
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
|
||||
public const string FailedToConfirm= "invoice_failedToConfirm";
|
||||
public const string Confirmed= "invoice_confirmed";
|
||||
public const string Completed= "invoice_completed";
|
||||
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public InvoiceEntity Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public PaymentEntity Payment { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
public ExplorerClientProvider(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
@ -32,14 +33,15 @@ namespace BTCPayServer
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
explorer.SetClient(httpClient);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user