Compare commits
1842 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
a8ba1ed1ed | |||
ff4056d4f3 | |||
ae152c3ffa | |||
e2ff33d7db | |||
ce94c05fd3 | |||
9cde4dc7e2 | |||
ca571cd756 | |||
e5eb0c79c0 | |||
43bd6587d3 | |||
3bb059ab74 | |||
4c963d6edf | |||
396bc7f7b4 | |||
2896a9b26f | |||
9267a45449 | |||
c430d470c4 | |||
3921a3ca22 | |||
1ff0a98d30 | |||
f0efd52cb7 | |||
bb8fa88688 | |||
4b976c13c1 | |||
f68d4efcdd | |||
fea247b218 | |||
f419c56a3c | |||
a5fca7a1c4 | |||
e18d0b5d51 | |||
9952cdca7f | |||
6278145374 | |||
84018a5caa | |||
d7785fe2d2 | |||
e1751c4d91 | |||
913da79ff4 | |||
a4fbb2de7e | |||
b5601ed5e6 | |||
42c4f15f22 | |||
6fbd9b2628 | |||
d04bfb58a2 | |||
cded2548f5 | |||
dcc859a86a | |||
9bec38559f | |||
2856454d41 | |||
b3c4fc4003 | |||
c2bbc04c4c | |||
db40c7bc32 | |||
60707fdbd1 | |||
f05614f4da | |||
a10c382bd4 | |||
da2fb876cb | |||
3c58bff803 | |||
a28814bc0e | |||
3cff8261ae | |||
b16e8f7b76 | |||
57ab001c0c | |||
3d85dace38 | |||
7a04c2974f | |||
d459839bf7 | |||
657cfe1b23 | |||
f4eaa0f01f | |||
1e2ffcadf0 | |||
dcc05af02e | |||
4b7f78f38b | |||
f94ff4cc74 | |||
b750663a1f | |||
4c4b76e995 | |||
da19d2c1a7 | |||
fb15c5b354 | |||
6ffe1cfcab | |||
87678c58ac | |||
feab4cc48a | |||
712946f512 | |||
a7bfceae05 | |||
8a26cd549a | |||
1cf3ce0617 | |||
73c65fada2 | |||
92ea923c03 | |||
e7db453717 | |||
10ee09f052 | |||
be7d91a138 | |||
3278c80d3f | |||
65e1edb0b8 | |||
e05c88370f | |||
15c29f8419 | |||
fc722731d3 | |||
1c9c564e90 | |||
872b60f8ea | |||
0d3364b3da | |||
fed53661b3 | |||
e86b4d89ca | |||
c5cb32f6dd | |||
deb56e16ec | |||
b5626ef01c | |||
e39d9067f2 | |||
43d34d5d35 | |||
7341be76bb | |||
0abd62dfe8 | |||
735012e3d7 | |||
b5efb8d2e6 | |||
1dbeabb716 | |||
671f9e56e2 | |||
dc6c189948 | |||
4501824f3f | |||
4568d2a98e | |||
f5d81334f8 | |||
f3ed90399b | |||
fada01cec9 | |||
1b4b9fb4cc | |||
6eeef8a866 | |||
24979a0af2 | |||
be1a44f018 | |||
9fcc2903fc | |||
06df63b283 | |||
0f1efc16f5 | |||
da8a06952c | |||
38d810cef7 | |||
393a3a2b8f | |||
aaddc580d1 | |||
957d478865 | |||
023913a852 | |||
6c51d83f61 | |||
0edaedb6ab | |||
13f21aa0d6 | |||
929a0c37bd | |||
058ccf56d0 | |||
162ac572da | |||
c7f3fdb46d | |||
29af07b3f9 | |||
758436a428 | |||
e0cadb4f62 | |||
013dfa1b61 | |||
e0f1c50534 | |||
d50dc2e68e | |||
8b5b18c97e | |||
f7383b4cc8 | |||
1bc32285ba | |||
f12114f9aa | |||
2b6faa8d20 | |||
45b7df6ac9 | |||
5a43ce2719 | |||
fe31dc8606 | |||
f0615482d9 | |||
03c47e6f7d | |||
4111b8a5a3 | |||
5b5a2e8c25 | |||
9ec0c23c52 | |||
9a5034c13c | |||
b1fcf4524a | |||
87d384dba5 | |||
4f5a8f7953 | |||
8728356698 | |||
9c30476fc8 | |||
09beb57eaf | |||
76a36d1829 | |||
0d4036efa2 | |||
cb4562aad5 | |||
0084d4766b | |||
74ddcfa01e | |||
ed36fba0d7 | |||
af015d435b | |||
ec59980e6f | |||
f0f4247c5d | |||
b562094956 | |||
4afd55c441 | |||
d7404f418d | |||
893410911c | |||
556b581b6a | |||
1685ccaca8 | |||
522abcfdfd | |||
ed1827ff28 | |||
214b2d1c1c | |||
322518e9dc | |||
ea2dd536b4 | |||
6a1eca760a | |||
29513d4ded | |||
57daf27fdd | |||
e698d90e3c | |||
86ca081030 | |||
14841ad7c9 | |||
2c6aa12aab | |||
ef4d39db3c | |||
7a566c477d | |||
85c40aef23 | |||
d00fa42553 | |||
e9e94f5e99 | |||
5e2d4407ca | |||
846bd08e20 | |||
a1a4eed860 | |||
1e582625f3 | |||
39b018fdf3 | |||
83304de1c6 | |||
5c8e03dcbf | |||
4dddc539f6 | |||
9950b781b4 | |||
7a32f692d1 | |||
d480be925b | |||
3775317047 | |||
500bdd9bf1 | |||
57bda24664 | |||
6401af00fe | |||
3b3a18bbbc | |||
b4e9dfeeaf | |||
16f5def245 | |||
26e7de534b | |||
a3ae694048 | |||
b04d70f141 | |||
101d6131c7 | |||
faabd68f6f | |||
aa72b814da | |||
0dcda0f289 | |||
a2b039f983 | |||
1a54f2d01a | |||
4276994265 | |||
2c6133b4f7 | |||
64181d1a93 | |||
25e9a27a78 | |||
f3edaf5160 | |||
7bfdf2d11d | |||
d2808cf662 | |||
b68fcec692 | |||
86644d38d7 | |||
52f60b0457 | |||
638b58ab48 | |||
1606f43609 | |||
ad1307746c | |||
22307b8a2b | |||
f1c244bf6a | |||
c425e0439d | |||
cdf78f0463 | |||
7d997f7d60 | |||
0edbbe6ae3 | |||
4f7bfbf451 | |||
cd416dac60 | |||
b58173967d | |||
4b743bb998 | |||
0af1adcfb8 | |||
c048e4eeaf | |||
64194896ba | |||
1d4472cc08 | |||
d8bc7ef4b8 | |||
27cea81cb2 | |||
b0d6216ffc | |||
060876d07f | |||
96b7373d88 | |||
c2f282f85c | |||
7afa726ddc | |||
6eb7bbf853 | |||
11a26c940d | |||
624252e4ad | |||
57bb980526 | |||
e0c718f4ba | |||
c58e015bfb | |||
648829644a | |||
1b0f8c7aca | |||
4f8e0b0393 | |||
466f65d6cd | |||
022b4f115d | |||
71f6aaabbd | |||
79b06bce42 | |||
480afebcd9 | |||
96721e95a2 | |||
883cd41232 | |||
3f48a478af | |||
8d3b45bdec | |||
bbd19a96ec | |||
ce17e3212a | |||
c3ea63c6ce | |||
1ee4bd9c92 | |||
e6bb6619e5 | |||
cc29d863d7 | |||
8cb2c93abd | |||
2187e05a10 | |||
4c07483383 | |||
65916755b6 | |||
3cefbc89e4 | |||
c40b47b1dd | |||
a2d17bfa7e | |||
cdf0c6d27d | |||
8154986102 | |||
203494e809 | |||
d49bbc95af | |||
c44132fd35 | |||
c75512303d | |||
97d50df13e | |||
0e1ef78af1 | |||
464ab30fea | |||
3ee1c05646 | |||
c54c39926b | |||
33d18a3278 | |||
97e564901e | |||
832069dd44 | |||
1ac17e96c3 | |||
d907031ec7 | |||
4b5af9cb5c | |||
0057146fee | |||
0c8925d2a2 | |||
b9e5b0d56e | |||
eb6dbd1247 | |||
fe8428b8b0 | |||
f2aa15310a | |||
1814cb2d6e | |||
94a6f20a05 | |||
22e700a53e | |||
cd78e559cf | |||
f0257fb8f7 | |||
34cdbf73f0 | |||
b291a6d25a | |||
fa7e974e73 | |||
976d9d0cda | |||
6ea2d9175d | |||
10ceddc709 | |||
5dd57c8064 | |||
a256dd3277 | |||
5ee9a92f1e | |||
65c7c85c14 | |||
27b686095c | |||
cd2fef0dab | |||
743288fa47 | |||
270ebead49 | |||
145e3bec83 | |||
563e931468 | |||
3113097c4f | |||
cdbbad1694 | |||
c9c2730409 | |||
310a9a6d59 | |||
1a1078782e | |||
73cb3dc4ee | |||
9eb36a8b40 | |||
6307aa8665 | |||
b9e8408db5 | |||
0879895678 | |||
a4ecf070b0 | |||
162d76206e | |||
5af14ef2ec | |||
7210eebeca | |||
25dbf6445f | |||
0828c60537 | |||
34deb17f3d | |||
06b02b8691 | |||
b7abc08c27 | |||
399ae2cd9e | |||
63fe0f6612 | |||
42d60ef84b | |||
1784c30787 | |||
ac8feceaf2 | |||
3d8c5195ae | |||
9a5259510b | |||
caecb26420 | |||
ecc8b3d9ed | |||
d313395751 | |||
9e698a8004 | |||
3c4c99ee42 | |||
d34ffc0d9a | |||
039303bfaa | |||
273cf1adc9 | |||
5feb520843 | |||
17e914778d | |||
db24ab792f | |||
42475ec7b7 | |||
4972f0ab7b | |||
07e13747cf | |||
2465eb7e36 | |||
4ddcd7a4c8 | |||
89d9658e82 | |||
66ecb32538 | |||
a22576da0a | |||
69bd888bab | |||
9b540273fc | |||
cfd083bed5 | |||
55c9314cdd | |||
8cafa8a483 | |||
448cc06a11 | |||
0780df4fd7 | |||
04174b7431 | |||
b7c58c2083 | |||
cd75fd6842 | |||
370951a3bd | |||
2c08b0137b | |||
724af44e41 | |||
1eee31e9f1 | |||
01cf579530 | |||
f72705935a | |||
a29ab6b6b0 | |||
4784518235 | |||
f8c88bd44f | |||
0d1d0d57f4 | |||
2bd1238668 | |||
d1fb51b412 | |||
279de1b869 | |||
ce9189caf8 | |||
431147784e | |||
0697b8bf86 | |||
5050b59014 | |||
665cf4c3b1 | |||
bac9ef4f93 | |||
98e81ab0fd | |||
6ce70237fc | |||
4f23fc99a1 | |||
d7fccae452 | |||
d7a5021ed2 | |||
63ec832667 | |||
8d95b9fa04 | |||
ada6f3b844 | |||
c8a26ce952 | |||
6cf80b7533 | |||
79df523bb2 | |||
e921f9757a | |||
b497d1871e | |||
c7cd029482 | |||
68f2cba60d | |||
5c4200b036 | |||
bc06114023 | |||
bed9737d64 | |||
556082c4c9 | |||
6a46d02fc6 | |||
d75e5b8b12 | |||
d293bc3947 | |||
e634700913 | |||
ce81136c88 | |||
a97ef2eee8 | |||
be33ebc168 | |||
789193a0c8 | |||
01792cf299 | |||
ff9265f721 | |||
8d61314852 | |||
1ce6ae8727 | |||
dec5dbc0d2 | |||
4e32dad1ea | |||
127ca7582f | |||
b98993f84b | |||
e35f074b66 | |||
ba3d13d56c | |||
ead67887ab | |||
437f27f107 | |||
8d41a8e98d | |||
7e6ab015a6 | |||
2583eb15ec | |||
1879ea55e8 | |||
f8bc3a5081 | |||
dd1a93ee0e | |||
093ae39e61 | |||
cac58808f0 | |||
a063f10778 | |||
3cf3aa63f6 | |||
011dd5574f | |||
365911286b | |||
fe5347aa86 | |||
f22c8a72cd | |||
eeb522fe7d | |||
f9e40b209a | |||
20635ea3d6 | |||
6cefd9c3e7 | |||
7062705d6f | |||
58b994e043 | |||
640ff36fa2 | |||
39ec5242d7 | |||
1c50210e61 | |||
a1ffda0151 | |||
fd15348551 | |||
989c99c550 | |||
bcf97b1474 | |||
67abbed66a | |||
eb01e91e13 | |||
12ceb9e0bc | |||
ecf03f90aa | |||
1747414a57 | |||
3a02f16c6e | |||
a6ee337ed0 | |||
559f535257 | |||
2952ccc7fd | |||
a0243fa569 | |||
789b9168ad | |||
7c29cb62ef | |||
f81ca1888d | |||
ed02e0f4d6 | |||
0a83f21af5 | |||
23a3c145ed | |||
4184c6c208 | |||
29c28b1841 | |||
de48fb4077 | |||
bcd79c5882 | |||
b8c513aa2b | |||
ad67f4ef18 | |||
2c0bcfc0ec | |||
0ba1072d54 | |||
f7fe855274 | |||
449738414b | |||
a34842585d | |||
eb882c2c46 | |||
ca65c6bd8f | |||
f97173e9e7 | |||
8fc1b0c856 | |||
cabd7c4e64 | |||
f8540dc78c | |||
b03d271f85 | |||
3770adb7d3 | |||
7fdf19ca22 | |||
4e776adb03 | |||
26db946392 | |||
d102c142b9 | |||
f7989541b9 | |||
b7f0ce18b3 | |||
e1dfbfe3b0 | |||
786d129452 | |||
a37a8e8fcd | |||
355989c278 | |||
af3dee95de | |||
70a6bd6a01 | |||
4afb0acc84 | |||
9afc143801 | |||
8e4943df65 | |||
9b3bd8343d | |||
ee4f83ddba | |||
c326998381 | |||
239a011e60 | |||
5ffe118159 | |||
6f07849e1d | |||
dbe5c62d11 | |||
199db01eaf | |||
a3c46c8f67 | |||
66a68d6180 | |||
be1128a886 | |||
d41a5a65a2 | |||
d5cab938ee | |||
9dddfb65f0 | |||
6bd5976d90 | |||
b3385bf901 | |||
bba268b5e2 | |||
70c98b6901 | |||
2d3b7fea2e | |||
3bdf1c9a00 | |||
a52665ea80 | |||
3d943d49e6 | |||
6ca8ba9231 | |||
75d685ae6c | |||
7b2ef9aec2 | |||
efe666b284 | |||
ca8af5047a | |||
cdc0b0d628 | |||
87e28b70fd | |||
b96f464e39 | |||
bca68986f3 | |||
272ac49872 | |||
5f05ca5ac6 | |||
7872b3ec55 | |||
27a0aebd12 | |||
366490516e | |||
9a92646d4d | |||
b002c49dac | |||
3f4ec9ba80 | |||
0290a5eacd | |||
744734a6a1 | |||
29f662f87c | |||
af21f9f10c | |||
efdc99b9d1 | |||
4458e63c1a | |||
3225745115 | |||
a325592106 | |||
01069ed583 | |||
0fc770bbb1 | |||
dfb79ef96e | |||
4ebffc8d43 | |||
c2dad08fef | |||
7c3ddf904c | |||
c3d73236e0 | |||
8a4da361fd | |||
57effe318b | |||
9325441693 | |||
180341576b | |||
e2533a93e3 | |||
14360bde78 | |||
d793265bed | |||
0a449e1e8e | |||
74ccc34c9c | |||
674cd1486d | |||
ce12e87b70 | |||
8f1324fdf3 | |||
3ab69046b0 | |||
6dc4bfaefe | |||
f460837f96 | |||
34d0d3e011 | |||
e57a488371 | |||
43be1e191f | |||
cfbcf0947a | |||
fcfba7f5e1 | |||
f4f9fabfd3 | |||
25208915eb | |||
eb975bf8fc | |||
21bbf49640 | |||
9339c7dff2 | |||
af0eb831a2 | |||
1fc9a1a54b | |||
3954ce2137 | |||
271de362cb | |||
d41474ebc8 | |||
5b0b3e30f4 | |||
48a95457b6 | |||
7c0b26174f | |||
f0145142a4 | |||
2848caff2e | |||
75f4a39ef2 | |||
f9f4d93191 | |||
9e05ad787f | |||
69050f7a56 | |||
de39fa0aea | |||
94ff77f2b2 | |||
bb7dc1ed4a | |||
c5e833ee79 | |||
4397591134 | |||
986c7b94f4 | |||
a6ef7387cf | |||
1743919cd4 | |||
131328b42c | |||
ad3b605148 | |||
f32e225fa6 | |||
95bdeacd93 | |||
07c2f6b810 | |||
8ff81f1648 | |||
c3ee43c228 | |||
d85da28ca7 | |||
042142396d | |||
fbc4ca89aa | |||
2e5d29064b | |||
ef0b8376d3 | |||
1fa1b74261 | |||
4f9e4116a2 | |||
82d8fda05f | |||
d4935263da | |||
e158d909fb | |||
de8147d5dd | |||
16f1791a9a | |||
8745c3f8c6 | |||
ec5b45cff6 | |||
1348197295 | |||
f2516854d8 | |||
062ca6e743 | |||
44b6997bb5 | |||
78b544f9ca | |||
81926b4450 | |||
a7ad71d492 | |||
18977f7265 | |||
8a88b44e98 | |||
c9e5fe42ba | |||
56dffbf514 | |||
0e1fac3773 | |||
e7c06880a8 | |||
39463a3202 | |||
36136f0f0f | |||
52e0845fc5 | |||
22e5b2869a | |||
fc3f32a4e0 | |||
3b0914e89e | |||
76cd9a7b25 | |||
0934bebf7b | |||
cd1a4c4749 | |||
8075273ec8 | |||
97b59be9bf | |||
b87ec4f3d9 | |||
3822358096 | |||
ba7e8cfe78 | |||
41978f1c59 | |||
e75e691404 | |||
a22216fd04 | |||
6900e16aa4 | |||
10c981b2a0 | |||
5f940df1b4 | |||
3f85918a0c | |||
e4299c09ea | |||
e864cf35f7 | |||
3652866660 | |||
0421004616 | |||
6936b034cb | |||
73ed4003a3 | |||
5cb8cdd511 | |||
195b5fdd1a | |||
d19b78b6cc | |||
9bbc05c3a7 | |||
7df3c86649 | |||
c6e0a923bb | |||
637fe1727b | |||
fd087bbeb8 | |||
2f515e1cc0 | |||
daf1a0a4bc | |||
bc8978182e | |||
84cd9e570f | |||
ead97a24bd | |||
415cde1629 | |||
b438312fde | |||
79ff2cb271 | |||
ed1464c405 | |||
f85631429b | |||
5ed56d1137 | |||
d7719d25b4 | |||
6267cccc3f | |||
fd5c4021f7 | |||
b8bf4d99ac | |||
0723eec508 | |||
7f01a12245 | |||
e1e3e5d953 | |||
18986faca8 | |||
2a68f8a90f | |||
659936577b | |||
85efc3b00c | |||
5efac45d46 | |||
c7dce280d7 | |||
04c6107196 | |||
54ce9b5dab | |||
cee955fb9d | |||
2e4b0daa48 | |||
e85ccfb47e | |||
b099f93c78 | |||
81afe397be | |||
f869c06aee | |||
75099b99d4 | |||
7b1b2a0f68 | |||
203c28df3d | |||
2e2c3cdec4 | |||
6f827c86a4 | |||
5aced90a3f | |||
4646f88e3a | |||
2b11cc1077 | |||
77b42eb085 | |||
7de067cd7a | |||
9da6df50b7 | |||
66b1623109 | |||
2432834f3d | |||
01fa483f95 | |||
1ddf47256f | |||
25fe32c3f8 | |||
ac9b8d03d7 | |||
8fdfb2c4f6 | |||
b1da136f77 | |||
9a6f85fa21 | |||
7308453a74 | |||
b798a17ef8 | |||
4b8899860e | |||
f46c8a0a0f | |||
48832f9ac3 | |||
9c798fc2e2 | |||
4704587f0a | |||
58e6b63fd7 | |||
3c76dfb584 | |||
10055d987d | |||
be49c60e83 | |||
14016e2f84 | |||
d7cb6f1cca | |||
0f63162254 | |||
21215dc537 | |||
20e147edfc | |||
1048dd516b | |||
42f44327f0 | |||
49200a4a9c | |||
7d71757de3 | |||
0fb492a70f | |||
7ccc1abb95 | |||
d61858e260 | |||
0ecd40f299 | |||
d9d4e74126 | |||
42d04bff61 | |||
f9cc29f014 | |||
992d359e79 | |||
1cc5427cbb | |||
6270a626fb | |||
40092b60fa | |||
5356b74490 | |||
e832ce5b4a | |||
a845ed88a7 | |||
560c1c3dc0 | |||
ecc5032bb2 | |||
325b359ff6 | |||
10fcc84379 | |||
149c29963d | |||
546c39a98e | |||
13223817a1 | |||
1b92314eb2 | |||
2b97808f1f | |||
8650446dcd | |||
7f24b89a80 | |||
e56ca73046 | |||
bf5062086c | |||
aa12167a6d | |||
9fa9f62d02 | |||
c9615b660e | |||
0ac51f479f | |||
37649fc77b | |||
ca4585eee9 | |||
83a1492cd4 | |||
a1af694acb | |||
6330c0f0d7 | |||
224c569ed1 | |||
c608987526 | |||
0c8f37ca19 | |||
aca67d6eae | |||
5dea0312ac | |||
f074007f67 | |||
88818ece29 | |||
fa0fa28949 | |||
08e31f6fe8 | |||
b976adeefe | |||
53c53b98e6 | |||
a171e00280 | |||
46f94d7175 | |||
2e555cac22 | |||
0d91b3286a | |||
396432b873 | |||
15c58434e8 | |||
daad1bdd25 | |||
c60966c725 | |||
fb57d8c3ce | |||
799ce74f65 | |||
8e38d7ceb4 | |||
a1c22e8071 | |||
6d8acf54d6 | |||
a500a89138 | |||
c6d44e7a89 | |||
9eb3aad072 | |||
9355454953 | |||
6467f06c54 | |||
b9b4b5ea39 | |||
e23243565f | |||
d3420532ae | |||
ade1b9d4eb | |||
fc278d12fc | |||
8e5ec822dc | |||
26aac66a76 | |||
a562e90bdb | |||
a0f3698701 | |||
02163f9482 | |||
b74fe171e2 | |||
2785bb4d9b | |||
5eac84d3a3 | |||
a0a2ab6fcd | |||
7730ead8e4 | |||
8eee0dd14c | |||
7dd88d8d8f | |||
56d1d3e645 | |||
c2308675b2 | |||
cb866a1c05 | |||
95290e8331 | |||
f5e62c775b | |||
f533309b49 | |||
d1c70a7cb3 | |||
2f8590ca7a | |||
08badbde56 | |||
8e38da80e0 | |||
cd2e3350b0 | |||
a0d2790491 | |||
8ca99e5635 | |||
5a2563ca7f | |||
a23cd28531 | |||
58a967b59e | |||
9bf0c20198 | |||
6b7ac0e000 | |||
188c0a9a86 | |||
c49479c8ad | |||
2072b6e136 | |||
08d82390b0 | |||
b845a545e2 | |||
db958b2401 | |||
7266420eec | |||
f36fbe7a76 | |||
8e279b110c | |||
d626870e46 | |||
df49b094d5 | |||
7d17bf7f2a | |||
e51f3dd1ae | |||
b810b88c6c | |||
39b34ff4ed | |||
f72fd63113 | |||
97eedc2c9f | |||
db222c53e3 | |||
61e919b88d | |||
d14040c142 | |||
13a3a581d8 | |||
f6dbae1cef | |||
ccbcda86ac | |||
b74e8cf756 | |||
8f8266f15d | |||
ab8d3f5813 | |||
08220dbea5 | |||
3b2cf2f1de | |||
c3beca27be | |||
28b820241f | |||
e985224092 | |||
f1c467aa7d | |||
ae7cfe90ab | |||
718a36ddd0 | |||
c0b903d79c | |||
48eaf906b0 | |||
f42fde970a | |||
59afebaa57 | |||
3e06e45054 | |||
fe55acb268 | |||
710dbb51f4 | |||
d426d66819 | |||
265cddc38b | |||
21b91ac8f7 | |||
e656813844 | |||
e8730f74be | |||
6d611d7d05 | |||
392f3a16f1 | |||
2b2e12b290 | |||
73cc75fe66 | |||
cc186fc8b3 | |||
632ad81b94 | |||
1d243910ae | |||
e624649cd8 | |||
e6ca07e9b5 | |||
b3d6435772 | |||
806474c8c6 | |||
1524fb4499 | |||
14b70ff35e | |||
c36a900627 | |||
d3befb5b86 | |||
7f0ce1f802 | |||
8342ad9175 | |||
acb2407654 | |||
b8a4f0c012 | |||
57bb3b231c | |||
e5d626e0fd | |||
e2c4c913ff | |||
09f97915d6 | |||
81328b2667 | |||
0d8affc68d | |||
da77d278fb | |||
5e9f6f3542 | |||
f337470f09 | |||
636224d0c8 | |||
b28b3ef4ff | |||
9e2e102ec4 | |||
9e16b83202 | |||
cbd40d49c1 | |||
1d051648b7 | |||
49cf804914 | |||
0f6ad75536 | |||
56eea18b2d | |||
b3698846c6 | |||
6806d96baa | |||
dc3b3077c2 | |||
936ae64ca3 | |||
3a0a5dbd7f | |||
ed4430ae7d | |||
9a0e4e35d9 | |||
5715dd2058 | |||
da4c132f9d | |||
303a617f9e | |||
3116ec9cb8 | |||
b3f4eab075 | |||
c98593b47b | |||
2c49d61682 | |||
21f5c94cff | |||
834ba4afab | |||
d690cd6b7b | |||
937bd07daa | |||
559b822111 | |||
7afb4c6b11 | |||
1c98a3a33d | |||
3320eb284e | |||
919fb60558 | |||
7796589105 | |||
de6d3198ff | |||
f1e971d047 | |||
acd98aad32 | |||
b0c810398c | |||
03a0044745 | |||
15684efdce | |||
b67a962d12 | |||
339cedadf7 | |||
e19d730fb7 | |||
649497e54f | |||
40eee0924a | |||
bbf5fb3c30 | |||
346cdf2431 | |||
030cb09af8 | |||
9f734349da | |||
2d5a861df0 | |||
061f428a54 | |||
9a539fd350 | |||
4149fe10e9 | |||
1014083160 | |||
dfa3167c18 | |||
7e09efb9a3 | |||
fb736c0d0f | |||
e4a7263e9b | |||
c52926f2b0 | |||
b6138b36be | |||
04bce3ae00 | |||
68ca162dd3 | |||
100bb02cd5 | |||
856249d52c | |||
309d6fdfe0 | |||
f289420364 | |||
f05e85de5f | |||
4138849546 | |||
7052e4e1dc | |||
ffb3e4f1fb | |||
297834be66 | |||
5924f1730c | |||
adc6bea4dc | |||
ef431f688f | |||
c8923af573 | |||
3d33ecf397 | |||
82d38da18e | |||
200e259b82 | |||
2d1f4e5e0a | |||
7fe64612ad | |||
5e452a679e | |||
10be8aec82 | |||
0e1a1fd2cd | |||
3f07010de8 | |||
2e45c8b190 | |||
b4f4401cdc | |||
a6b92a0dd5 | |||
2f3238c65e | |||
65f5a38b4a | |||
271cbf682f | |||
a634593903 | |||
af94de93d1 | |||
35f669aa15 | |||
4795bd8108 | |||
29aed99fd1 | |||
aa4519ac30 | |||
752133b01c | |||
fe0c21ba08 | |||
90904a6b5e | |||
8e3f7ea68d | |||
a239104a28 | |||
3bc232e1da | |||
a1ee09cd85 | |||
b898cc030c | |||
0602353dd2 | |||
9d406923ae | |||
aa8565e3cc | |||
5de330b1f9 | |||
66597aed46 | |||
3071826f06 | |||
c3684eb064 | |||
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
770bed54d1 | |||
774817d4ac | |||
b8068b2ae8 | |||
3007a6bbc8 | |||
1c0c8fece2 | |||
c52eee47f0 | |||
f88c98b9d9 | |||
b0e9e10f7e | |||
39d47e33f6 | |||
4b7b6c6327 | |||
a59edc5e8c | |||
5ba322ea6a | |||
b47b4b10cb | |||
c88f391935 | |||
26d3178e93 | |||
1ad27c7827 | |||
4200a8eed5 | |||
daceb7af8e | |||
f703b53bce | |||
2762224f0f | |||
86fc64d184 | |||
726cd6fd49 | |||
be1c4666e0 | |||
b75dfc4191 | |||
97815f8daf | |||
af16e1db69 | |||
5f6913b3a2 | |||
2b31af80cb | |||
3f9889d374 | |||
f8189c64a4 | |||
c9b5f89f17 | |||
ecb82f2cc9 | |||
f340c6eb7f | |||
ba0e080816 | |||
bb3d107309 | |||
8517b222bf | |||
aed32204b5 | |||
6b4eeff3f1 | |||
e3cc589ebb | |||
4a152e8ffc | |||
d54a9474d1 | |||
98472211fc | |||
099c9fa1f9 | |||
5226b77ffc | |||
290779ee39 | |||
4f39a8060c | |||
92caa98dfb | |||
df7bb9e2f8 | |||
02a039d695 | |||
b5e4c803aa | |||
2b7c70622f | |||
88779e7129 | |||
6beb7abfd2 | |||
a1ebedc0d1 | |||
d5ad0cdb39 | |||
39fb8dbb6a | |||
58194cb060 | |||
ef165e15bf | |||
bf871c46ec | |||
52331e057f | |||
b59021a0be | |||
8596e16feb | |||
223558c01d | |||
3a91965187 | |||
55d50af39d | |||
3ff293ab7f | |||
7bcf2b5472 | |||
983f34814f | |||
a33e20b46b | |||
bafdcb04ed | |||
cb4468d3b3 | |||
de6f0008a6 | |||
7618eacef1 | |||
842e083ebe | |||
1c510df3fc | |||
c80ffe396e | |||
401a31e5c2 | |||
2df60bd121 | |||
6d10c8a6c1 | |||
44898b5e23 | |||
c0f53db561 | |||
133fb96d28 | |||
98b7ad62af | |||
3069fe0dd9 | |||
729555b96f | |||
b4040ba7ad | |||
863752a471 | |||
6ae9d13c43 | |||
0c735f4e29 | |||
76d50b018b | |||
31672a2587 | |||
a048494f34 | |||
c513d6bd44 | |||
c3d37b1f78 | |||
5910644cda | |||
a16cd3e287 | |||
e3a0122eb3 | |||
1cda0eff16 | |||
6003aa4236 | |||
8753dd15de | |||
6ae6335c6d | |||
e3a1eed8b3 | |||
eb44203475 | |||
80e878c2f5 | |||
6cb1649fc2 | |||
63fceed5f4 | |||
781b2885cc | |||
2f9afda0ab | |||
108146ca92 | |||
bb46294a6d | |||
f8aad6ac80 | |||
658d1f1df0 | |||
ee3144f34a | |||
9a34fe46fb | |||
766d96c02d | |||
7445c89773 | |||
28ac5608a5 | |||
44c925a4ba | |||
51cd89f177 | |||
ab188ad54f | |||
513835ed0f | |||
a863812f90 | |||
a37fdde214 | |||
d5ef36fe50 | |||
7430ceb23d | |||
395b550c21 | |||
774565b121 | |||
72d1344002 | |||
b4ee5dcb0d | |||
a0f0ff0bf1 | |||
db2cc8f951 | |||
24007f1515 | |||
3d7445f359 | |||
34760afe77 | |||
417209b057 | |||
9026378b86 | |||
9b3dca1683 | |||
cde593a935 | |||
f0755260a6 | |||
582e1eb4f8 | |||
aaadda3e0f | |||
9d7f5b5b6e | |||
99040597dc | |||
d9794216dd | |||
84bb6056d3 | |||
dfed2daa8e | |||
1521ec8071 | |||
bf7ae178ef | |||
dc7f96c6da | |||
c6959bb0bc | |||
d4dd6c84bc | |||
e59678360c | |||
1b6fa0c7d8 | |||
95a5936daf | |||
477d4117ce | |||
444f119e50 | |||
fa13a2874e | |||
24ce325e31 | |||
a52a1901c4 | |||
45aee607e3 | |||
c263016939 | |||
741915b1f8 | |||
6f2534ba82 | |||
43635071d9 | |||
22f06ecd4e | |||
7efe83eba8 | |||
a5b732e197 | |||
f404aaf768 | |||
e1f8177834 | |||
cff391a7a9 | |||
9cd7608a53 | |||
6950a06532 | |||
0e6c2ec556 | |||
479fc50d9a | |||
a29a8f7ed9 | |||
83cf637f9d | |||
5dbb4bf6be | |||
f1f227b746 | |||
b96cac16c6 | |||
f58fdafdcd | |||
b7b39f8284 | |||
7a173a6692 | |||
b042f98f0f | |||
0bb260bec9 | |||
024ab8ff69 | |||
50cb61f3bd | |||
f2befb7c48 | |||
dd80263490 | |||
73c44f8726 | |||
ca85095273 | |||
b5196aa541 | |||
b649ae0a1e | |||
4deb7c3270 | |||
b10da976c7 | |||
27a2e614a5 | |||
e7a931e9ba | |||
979bc31868 | |||
33cf4066d8 | |||
009444951a | |||
9e27e14692 | |||
bc00633135 | |||
9a7aa181ed | |||
6cc90cbeb4 | |||
59de852484 | |||
c9d5e9ac05 | |||
59ce3b5fc0 | |||
b71f9d0a08 | |||
55a4c3c08d | |||
c79db1dc99 | |||
a5bd27661b | |||
9a0d0a7124 | |||
454939adad | |||
37c02d2539 | |||
5f8407b4b1 | |||
abeb10cc8c | |||
4dca81403b | |||
6ba6a34df2 | |||
6d14fe9c30 | |||
c0c4637c77 | |||
752d34f603 | |||
0009ed0921 | |||
953de22233 | |||
1477aef39c | |||
2cca8f82e1 | |||
9094af025d | |||
0ba608bd1e | |||
67f5d497d5 | |||
3cc614c024 | |||
7c20bab1c5 | |||
a2a3f43fd0 | |||
9f17e3e1f8 | |||
c1a2fc22f4 | |||
f1f19369a3 | |||
e49f25af09 | |||
bc28571174 | |||
2beae1dcd3 | |||
39a0b1e29e | |||
0f603ffb0a | |||
8fe5835e09 | |||
5fed7a3a0c | |||
28ea694791 | |||
45b0991841 | |||
505d9904af | |||
da385f9295 | |||
214de77a41 | |||
7b2db3755e | |||
44791cc9f3 | |||
a14b94c96f | |||
db1cf5c2ce | |||
1a060a6c7b | |||
ff719fbe2d | |||
94e9ab7f67 | |||
06a96e8b77 | |||
d43c3dc968 | |||
e3c5efa929 | |||
8db9d93d23 | |||
517bb94b8b | |||
c804f55d82 | |||
c96a25c9b9 | |||
4c57726be7 | |||
8f723d7131 | |||
a7e10c0fb9 | |||
15e73e1cad | |||
a17192ee99 | |||
27200d1fb0 | |||
9ddceae824 | |||
d1961e0938 | |||
033432d6fb | |||
c98f0ba55b | |||
76993d2532 | |||
62e3f2d8e1 | |||
9806cab090 | |||
0d9fbe2d41 |
98
.circleci/config.yml
Normal file
98
.circleci/config.yml
Normal file
@ -0,0 +1,98 @@
|
||||
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
|
||||
docker-compose run 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]+)*/
|
@ -121,3 +121,4 @@ bower_components
|
||||
output
|
||||
|
||||
.vs
|
||||
**/launchSettings.json
|
||||
|
149
.editorconfig
Normal file
149
.editorconfig
Normal file
@ -0,0 +1,149 @@
|
||||
# editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Default settings:
|
||||
# A newline ending every file
|
||||
# Use 4 spaces as indentation
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[project.json]
|
||||
indent_size = 2
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
# New line preferences
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_within_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
# avoid this. unless absolutely necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# only use var when it's obvious what the variable type is
|
||||
csharp_style_var_for_built_in_types = false:none
|
||||
csharp_style_var_when_type_is_apparent = false:none
|
||||
csharp_style_var_elsewhere = false:suggestion
|
||||
|
||||
# use language keywords instead of BCL types
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# name all constant fields using PascalCase
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
# internal and private fields should be _camelCase
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
|
||||
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
|
||||
|
||||
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
|
||||
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
|
||||
|
||||
# Code style defaults
|
||||
dotnet_sort_system_directives_first = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = false
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Pattern matching
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
|
||||
# Null checking preferences
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# C++ Files
|
||||
[*.{cpp,h,in}]
|
||||
curly_bracket_next_line = true
|
||||
indent_brace_style = Allman
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml build files
|
||||
[*.builds]
|
||||
indent_size = 2
|
||||
|
||||
# Xml files
|
||||
[*.{xml,stylecop,resx,ruleset}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,config,nuspec}]
|
||||
indent_size = 2
|
||||
|
||||
# Shell scripts
|
||||
[*.sh]
|
||||
end_of_line = lf
|
||||
[*.{cmd, bat}]
|
||||
end_of_line = crlf
|
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -287,3 +287,9 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
/BTCPayServer/Build/dockerfiles
|
||||
|
||||
# Bundling JS/CSS
|
||||
BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
||||
.vscode
|
||||
|
@ -1,19 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<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.3.0-preview-20170720-02" />
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<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>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -23,6 +25,12 @@
|
||||
<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,7 +1,14 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Linq;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -27,131 +34,234 @@ using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
public enum TestDatabases
|
||||
{
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
|
||||
public BTCPayServerTester(string scope)
|
||||
{
|
||||
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
|
||||
public Uri NBXplorerUri
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public BTCPayServerTester(string scope)
|
||||
{
|
||||
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
|
||||
public ExtKey HDPrivateKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri NBXplorerUri
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri LTCNBXplorerUri { get; set; }
|
||||
|
||||
IWebHost _Host;
|
||||
public int Port
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if(!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
public string MySQL
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
HDPrivateKey = new ExtKey();
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"regtest=1");
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"explorer.cookiefile={CookieFile}");
|
||||
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
|
||||
if(Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
IWebHost _Host;
|
||||
public int Port
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
|
||||
public TestDatabases TestDatabase
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddProvider(Logs.LogProvider);
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
_Host.Start();
|
||||
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
|
||||
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
|
||||
watcher.PollInterval = TimeSpan.FromMilliseconds(500);
|
||||
}
|
||||
public bool MockRates { get; set; } = true;
|
||||
|
||||
public BTCPayServerRuntime Runtime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public void Start()
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
||||
public T GetController<T>(string userId = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if(userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
context.RequestServices = provider;
|
||||
|
||||
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
|
||||
httpAccessor.HttpContext = context;
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"{chain.ToLowerInvariant()}=1");
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"chains=btc,ltc");
|
||||
|
||||
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
|
||||
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"btc.explorer.cookiefile=0");
|
||||
|
||||
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
|
||||
controller.ControllerContext = new ControllerContext()
|
||||
{
|
||||
HttpContext = context
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Host != null)
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
|
||||
config.AppendLine($"mysql=" + MySQL);
|
||||
else if (!String.IsNullOrEmpty(Postgres))
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddProvider(Logs.LogProvider);
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
if (MockRates)
|
||||
{
|
||||
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
|
||||
rateProvider.Providers.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
BidAsk = new BidAsk(5000m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(4500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
|
||||
BidAsk = new BidAsk(0.001m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.Providers.Add("coinaverage", coinAverageMock);
|
||||
|
||||
var bitflyerMock = new MockRateProvider();
|
||||
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bitflyer",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
|
||||
BidAsk = new BidAsk(700000m)
|
||||
});
|
||||
rateProvider.Providers.Add("bitflyer", bitflyerMock);
|
||||
|
||||
var quadrigacx = new MockRateProvider();
|
||||
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "quadrigacx",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(6000m)
|
||||
});
|
||||
rateProvider.Providers.Add("quadrigacx", quadrigacx);
|
||||
|
||||
var bittrex = new MockRateProvider();
|
||||
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bittrex",
|
||||
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
|
||||
BidAsk = new BidAsk(0.004m)
|
||||
});
|
||||
rateProvider.Providers.Add("bittrex", bittrex);
|
||||
}
|
||||
}
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null, Claim[] additionalClaims = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1", Port);
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
if (additionalClaims != null)
|
||||
claims.AddRange(additionalClaims);
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), Policies.CookieAuthentication));
|
||||
}
|
||||
if (storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
context.RequestServices = provider;
|
||||
|
||||
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
|
||||
httpAccessor.HttpContext = context;
|
||||
|
||||
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
|
||||
|
||||
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
|
||||
controller.ControllerContext = new ControllerContext()
|
||||
{
|
||||
HttpContext = context
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_Host != null)
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
253
BTCPayServer.Tests/ChangellyTests.cs
Normal file
253
BTCPayServer.Tests/ChangellyTests.cs
Normal file
@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChangellyTests
|
||||
{
|
||||
public ChangellyTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
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.ChangellySettings);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
|
||||
updateModel.ApiSecret);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
|
||||
updateModel.ChangellyMerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleChangellyPaymentMethod()
|
||||
{
|
||||
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 UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
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 = CreateDefaultChangellyParams(false);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
updateModel.Enabled = true;
|
||||
//test with enabled method
|
||||
|
||||
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))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
//save changelly settings
|
||||
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);
|
||||
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())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
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);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer.Tests/ChargeTester.cs
Normal file
25
BTCPayServer.Tests/ChargeTester.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChargeTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public ChargeTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
252
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
252
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
@ -0,0 +1,252 @@
|
||||
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.Crowdfund;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Hubs;
|
||||
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;
|
||||
|
||||
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)
|
||||
}));
|
||||
|
||||
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)
|
||||
}));
|
||||
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)
|
||||
}));
|
||||
|
||||
//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)
|
||||
}));
|
||||
|
||||
|
||||
//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)
|
||||
}));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCrowdfundModel()
|
||||
{
|
||||
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;
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
|
||||
|
||||
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount );
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal( 0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -8,68 +8,67 @@ 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();
|
||||
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);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseUrls("http://127.0.0.1:" + port)
|
||||
.Build();
|
||||
_Host.Start();
|
||||
}
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseUrls("http://127.0.0.1:" + port)
|
||||
.Build();
|
||||
_Host.Start();
|
||||
}
|
||||
|
||||
public Uri GetUri()
|
||||
{
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
public Uri GetUri()
|
||||
{
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>();
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>();
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
{
|
||||
_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");
|
||||
}
|
||||
}
|
||||
public async Task<JObject> GetNextRequest()
|
||||
{
|
||||
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject req = null;
|
||||
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
|
||||
!_Requests.Reader.TryRead(out req))
|
||||
{
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Closed.Cancel();
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Closed.Cancel();
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
FROM microsoft/dotnet:2.0.0-sdk
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
FROM microsoft/dotnet:2.1.500-sdk-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
|
||||
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
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"]
|
||||
|
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class LightningDTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
}
|
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
public class LndMockTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndClient(Swagger, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
@ -6,84 +6,88 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests.Logging
|
||||
{
|
||||
public interface ILog
|
||||
{
|
||||
void LogInformation(string msg);
|
||||
}
|
||||
public interface ILog
|
||||
{
|
||||
void LogInformation(string msg);
|
||||
}
|
||||
|
||||
public class XUnitLogProvider : ILoggerProvider
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLogProvider(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new XUnitLog(_Helper) { Name = categoryName };
|
||||
}
|
||||
public class XUnitLogProvider : ILoggerProvider
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLogProvider(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new XUnitLog(_Helper) { Name = categoryName };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class XUnitLog : ILog, ILogger, IDisposable
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLog(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
public class XUnitLog : ILog, ILogger, IDisposable
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLog(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append(formatter(state, exception));
|
||||
if(exception != null)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(exception.ToString());
|
||||
}
|
||||
LogInformation(builder.ToString());
|
||||
}
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if(msg != null)
|
||||
_Helper.WriteLine(Name + ": " + msg);
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
{
|
||||
public static ILog Tester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static XUnitLogProvider LogProvider
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append(formatter(state, exception));
|
||||
if (exception != null)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(exception.ToString());
|
||||
}
|
||||
LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if (msg != null)
|
||||
try
|
||||
{
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
{
|
||||
public static ILog Tester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static XUnitLogProvider LogProvider
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
64
BTCPayServer.Tests/MockDelay.cs
Normal file
64
BTCPayServer.Tests/MockDelay.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
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 void Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
wait.CTS.TrySetResult(true);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AdvanceMilliseconds(long milli)
|
||||
{
|
||||
Advance(TimeSpan.FromMilliseconds(milli));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,38 +6,38 @@ using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class UrlHelperMock : IUrlHelper
|
||||
{
|
||||
Uri _BaseUrl;
|
||||
public UrlHelperMock(Uri baseUrl)
|
||||
{
|
||||
_BaseUrl = baseUrl;
|
||||
}
|
||||
public ActionContext ActionContext => throw new NotImplementedException();
|
||||
public class UrlHelperMock : IUrlHelper
|
||||
{
|
||||
Uri _BaseUrl;
|
||||
public UrlHelperMock(Uri baseUrl)
|
||||
{
|
||||
_BaseUrl = baseUrl;
|
||||
}
|
||||
public ActionContext ActionContext => throw new NotImplementedException();
|
||||
|
||||
public string Action(UrlActionContext actionContext)
|
||||
{
|
||||
return $"{_BaseUrl}mock";
|
||||
}
|
||||
public string Action(UrlActionContext actionContext)
|
||||
{
|
||||
return $"{_BaseUrl}mock";
|
||||
}
|
||||
|
||||
public string Content(string contentPath)
|
||||
{
|
||||
return $"{_BaseUrl}{contentPath}";
|
||||
}
|
||||
public string Content(string contentPath)
|
||||
{
|
||||
return $"{_BaseUrl}{contentPath}";
|
||||
}
|
||||
|
||||
public bool IsLocalUrl(string url)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
public bool IsLocalUrl(string url)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public string Link(string routeName, object values)
|
||||
{
|
||||
return _BaseUrl.AbsoluteUri;
|
||||
}
|
||||
public string Link(string routeName, object values)
|
||||
{
|
||||
return _BaseUrl.AbsoluteUri;
|
||||
}
|
||||
|
||||
public string RouteUrl(UrlRouteContext routeContext)
|
||||
{
|
||||
return _BaseUrl.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
public string RouteUrl(UrlRouteContext routeContext)
|
||||
{
|
||||
return _BaseUrl.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,105 +8,105 @@ using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ProcessLauncher
|
||||
{
|
||||
string _CurrentDirectory;
|
||||
public string CurrentDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrentDirectory;
|
||||
}
|
||||
}
|
||||
public ProcessLauncher()
|
||||
{
|
||||
_CurrentDirectory = Directory.GetCurrentDirectory();
|
||||
}
|
||||
public bool GoTo(string[] directories, bool createIfNotExists = false)
|
||||
{
|
||||
var original = _CurrentDirectory;
|
||||
foreach(var dir in directories)
|
||||
{
|
||||
var newDirectory = Path.Combine(_CurrentDirectory, dir);
|
||||
if(!Directory.Exists(newDirectory))
|
||||
{
|
||||
if(createIfNotExists)
|
||||
Directory.CreateDirectory(newDirectory);
|
||||
else
|
||||
{
|
||||
_CurrentDirectory = original;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_CurrentDirectory = newDirectory;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public class ProcessLauncher
|
||||
{
|
||||
string _CurrentDirectory;
|
||||
public string CurrentDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrentDirectory;
|
||||
}
|
||||
}
|
||||
public ProcessLauncher()
|
||||
{
|
||||
_CurrentDirectory = Directory.GetCurrentDirectory();
|
||||
}
|
||||
public bool GoTo(string[] directories, bool createIfNotExists = false)
|
||||
{
|
||||
var original = _CurrentDirectory;
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var newDirectory = Path.Combine(_CurrentDirectory, dir);
|
||||
if (!Directory.Exists(newDirectory))
|
||||
{
|
||||
if (createIfNotExists)
|
||||
Directory.CreateDirectory(newDirectory);
|
||||
else
|
||||
{
|
||||
_CurrentDirectory = original;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_CurrentDirectory = newDirectory;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Stack<string> _Directories = new Stack<string>();
|
||||
public void PushDirectory()
|
||||
{
|
||||
_Directories.Push(_CurrentDirectory);
|
||||
}
|
||||
Stack<string> _Directories = new Stack<string>();
|
||||
public void PushDirectory()
|
||||
{
|
||||
_Directories.Push(_CurrentDirectory);
|
||||
}
|
||||
|
||||
public void PopDirectory()
|
||||
{
|
||||
_CurrentDirectory = _Directories.Pop();
|
||||
}
|
||||
public void PopDirectory()
|
||||
{
|
||||
_CurrentDirectory = _Directories.Pop();
|
||||
}
|
||||
|
||||
public bool GoTo(string directory, bool createIfNotExists = false)
|
||||
{
|
||||
return GoTo(new[] { directory }, createIfNotExists);
|
||||
}
|
||||
public bool GoTo(string directory, bool createIfNotExists = false)
|
||||
{
|
||||
return GoTo(new[] { directory }, createIfNotExists);
|
||||
}
|
||||
|
||||
public void Run(string processName, string args)
|
||||
{
|
||||
Start(processName, args).WaitForExit();
|
||||
}
|
||||
public void Run(string processName, string args)
|
||||
{
|
||||
Start(processName, args).WaitForExit();
|
||||
}
|
||||
|
||||
public Process Start(string processName, string args)
|
||||
{
|
||||
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = _CurrentDirectory,
|
||||
FileName = processName,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.Start();
|
||||
process.BeginErrorReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
}
|
||||
catch(Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
|
||||
return process;
|
||||
}
|
||||
public Process Start(string processName, string args)
|
||||
{
|
||||
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = _CurrentDirectory,
|
||||
FileName = processName,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.Start();
|
||||
process.BeginErrorReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
}
|
||||
catch (Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
|
||||
return process;
|
||||
}
|
||||
|
||||
public void AssertExists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
Assert.True(File.Exists(path), $"The file {path} should exist");
|
||||
}
|
||||
public void AssertExists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
Assert.True(File.Exists(path), $"The file {path} should exist");
|
||||
}
|
||||
|
||||
public bool Exists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
return File.Exists(path);
|
||||
}
|
||||
}
|
||||
public bool Exists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
return File.Exists(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"BTCPayServer.Tests": {
|
||||
"commandName": "Project"
|
||||
"profiles": {
|
||||
"BTCPayServer.Tests": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
# How to run the tests
|
||||
# How to be started for development
|
||||
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
|
||||
|
||||
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
docker-compose up dev
|
||||
```
|
||||
|
||||
You can run the tests while it is running through your favorite IDE, or with
|
||||
@ -19,14 +23,58 @@ Once you want to stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
If you want to stop, and remove all existing data
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
docker-compose down --v
|
||||
```
|
||||
|
||||
The Bitcoin RPC server is exposed to the host, for example, you can send 0.23111090 BTC to mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf.
|
||||
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
|
||||
|
||||
## How to manually test payments
|
||||
|
||||
### Using the test bitcoin-cli
|
||||
|
||||
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
|
||||
```
|
||||
./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
### Using the test litecoin-cli
|
||||
|
||||
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
|
||||
|
||||
### Using the test lightning-cli
|
||||
|
||||
If you are using Linux:
|
||||
```
|
||||
./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
|
||||
```
|
||||
|
||||
If you get this message:
|
||||
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=43782 -rpcuser=ceiwHEbqWI83 -rpcpassword=DwubwWsoo3 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
{ "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } }
|
||||
```
|
||||
|
||||
Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry.
|
||||
|
||||
## FAQ
|
||||
|
||||
`docker-compose up dev` failed or tests are not passing, what should I do?
|
||||
|
||||
1. Run `docker-compose down --v` (this will reset your test environment)
|
||||
2. Run `docker-compose pull` (this will ensure you have the lastest images)
|
||||
3. Run again with `docker-compose up dev`
|
||||
|
||||
If you still have issues, try to restart docker.
|
||||
|
194
BTCPayServer.Tests/RateRulesTest.cs
Normal file
194
BTCPayServer.Tests/RateRulesTest.cs
Normal file
@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using Xunit;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void SecondDuplicatedRuleIsIgnored()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = 1.1");
|
||||
builder.AppendLine("DOGE_X = 1.2");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
|
||||
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
|
||||
rule.Reevaluate();
|
||||
Assert.True(!rule.HasError);
|
||||
Assert.Equal(1.1m, rule.BidAsk.Ask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
// Check happy path
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("// Some cool comments");
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("// Some other cool comments");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
|
||||
Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules));
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
Assert.Equal(
|
||||
"// Some cool comments\n" +
|
||||
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
|
||||
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
|
||||
"// Some other cool comments\n" +
|
||||
"BTC_USD = gdax(BTC_USD);\n" +
|
||||
"BTC_X = coinbase(BTC_X);\n" +
|
||||
"X_X = coinaverage(X_X) * 1.02;",
|
||||
rules.ToString());
|
||||
var tests = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
rules.Spread = 0.2m;
|
||||
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
|
||||
////////////////
|
||||
|
||||
// Check errors conditions
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
tests = new[]
|
||||
{
|
||||
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
//////////////////
|
||||
|
||||
// Check if we can resolve exchange rates
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
var tests2 = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||
};
|
||||
foreach (var test in tests2)
|
||||
{
|
||||
var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair));
|
||||
Assert.Equal(test.Expected, rule.ToString());
|
||||
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
|
||||
}
|
||||
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
|
||||
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
|
||||
rule2.Reevaluate();
|
||||
Assert.True(rule2.HasError);
|
||||
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X");
|
||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||
builder.AppendLine("DOGE_BTC = 2000");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rules.Spread = 0.1m;
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
|
||||
|
||||
// Test inverse
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
|
||||
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
|
||||
////////
|
||||
|
||||
// Make sure kraken is not converted to CurrencyPair
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
|
||||
// Make sure can handle pairs
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(6000m, rule2.BidAsk.Bid);
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("EUR_CDNT = 10");
|
||||
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
|
||||
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / 10", rule2.ToString(false));
|
||||
|
||||
// Make sure an inverse can be solved on an exchange
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -16,221 +17,153 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ServerTester : IDisposable
|
||||
{
|
||||
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
|
||||
{
|
||||
return new ServerTester(scope);
|
||||
}
|
||||
public class ServerTester : IDisposable
|
||||
{
|
||||
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
|
||||
{
|
||||
return new ServerTester(scope);
|
||||
}
|
||||
|
||||
string _Directory;
|
||||
public ServerTester(string scope)
|
||||
{
|
||||
_Directory = scope;
|
||||
}
|
||||
string _Directory;
|
||||
public ServerTester(string scope)
|
||||
{
|
||||
_Directory = scope;
|
||||
if (Directory.Exists(_Directory))
|
||||
Utils.DeleteDirectory(_Directory);
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
public bool Dockerized
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
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);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if(Directory.Exists(_Directory))
|
||||
Utils.DeleteDirectory(_Directory);
|
||||
if(!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
|
||||
FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true"));
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
|
||||
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.Start();
|
||||
}
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
private string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
|
||||
public TestAccount NewAccount()
|
||||
{
|
||||
return new TestAccount(this);
|
||||
}
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver"),
|
||||
IntegratedLightning = MerchantCharge.Client.Uri
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
|
||||
}
|
||||
|
||||
public bool FakeCallback
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool Dockerized
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public void Start()
|
||||
{
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task EnsureChannelsSetup()
|
||||
{
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
}
|
||||
|
||||
class MockHttpRequest : HttpRequest
|
||||
{
|
||||
Uri serverUri;
|
||||
public MockHttpRequest(Uri serverUri)
|
||||
{
|
||||
this.serverUri = serverUri;
|
||||
}
|
||||
public override HttpContext HttpContext => throw new NotImplementedException();
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
{
|
||||
yield return CustomerLightningD;
|
||||
}
|
||||
|
||||
public override string Method
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Scheme
|
||||
{
|
||||
get => serverUri.Scheme;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override bool IsHttps
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override HostString Host
|
||||
{
|
||||
get => new HostString(serverUri.Host, serverUri.Port);
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString PathBase
|
||||
{
|
||||
get => "";
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString Path
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override QueryString QueryString
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override IQueryCollection Query
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Protocol
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||
{
|
||||
yield return MerchantLightningD;
|
||||
yield return MerchantLnd.Client;
|
||||
}
|
||||
|
||||
public override IHeaderDictionary Headers => throw new NotImplementedException();
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override IRequestCookieCollection Cookies
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override long? ContentLength
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string ContentType
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override Stream Body
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public async Task SendLightningPaymentAsync(Invoice invoice)
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public override bool HasFormContentType => throw new NotImplementedException();
|
||||
public ILightningClient CustomerLightningD { get; set; }
|
||||
|
||||
public override IFormCollection Form
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public ILightningClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost.
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
public void SimulateCallback(BitcoinAddress address = null)
|
||||
{
|
||||
if(!FakeCallback) //The callback of NBXplorer should work
|
||||
return;
|
||||
public TestAccount NewAccount()
|
||||
{
|
||||
return new TestAccount(this);
|
||||
}
|
||||
|
||||
var req = new MockHttpRequest(PayTester.ServerUri);
|
||||
var controller = PayTester.GetController<CallbackController>();
|
||||
if(address != null)
|
||||
{
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
var match = new TransactionMatch();
|
||||
match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey });
|
||||
var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json");
|
||||
var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult();
|
||||
public RPCClient LTCExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
HttpRequestMessage message = new HttpRequestMessage();
|
||||
message.Method = HttpMethod.Post;
|
||||
message.RequestUri = uri;
|
||||
message.Content = content;
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ExplorerClient LTCExplorerClient { get; set; }
|
||||
|
||||
_Http.SendAsync(message).GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult();
|
||||
HttpRequestMessage message = new HttpRequestMessage();
|
||||
message.Method = HttpMethod.Post;
|
||||
message.RequestUri = uri;
|
||||
_Http.SendAsync(message).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Network.RegTest;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
@ -10,83 +11,157 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class TestAccount
|
||||
{
|
||||
ServerTester parent;
|
||||
public TestAccount(ServerTester parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
|
||||
}
|
||||
ServerTester parent;
|
||||
public TestAccount(ServerTester parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
|
||||
}
|
||||
|
||||
public void GrantAccess()
|
||||
{
|
||||
GrantAccessAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
public void GrantAccess()
|
||||
{
|
||||
GrantAccessAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Register()
|
||||
{
|
||||
RegisterAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
public void Register()
|
||||
{
|
||||
RegisterAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public BitcoinExtKey ExtKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public BitcoinExtKey ExtKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
var store = await CreateStoreAsync();
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
public StoresController CreateStore()
|
||||
{
|
||||
return CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<StoresController> CreateStoreAsync()
|
||||
{
|
||||
ExtKey = new ExtKey().GetWif(parent.Network);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
await store.UpdateStore(StoreId, new StoreViewModel()
|
||||
{
|
||||
DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]",
|
||||
SpeedPolicy = SpeedPolicy.MediumSpeed
|
||||
}, "Save");
|
||||
return store;
|
||||
}
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
await CreateStoreAsync();
|
||||
var store = this.GetController<StoresController>();
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
public void CreateStore()
|
||||
{
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
var account = parent.PayTester.GetController<AccountController>();
|
||||
await account.Register(new RegisterViewModel()
|
||||
{
|
||||
Email = Guid.NewGuid() + "@toto.com",
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
});
|
||||
UserId = account.RegisteredUserId;
|
||||
}
|
||||
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 Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
}
|
||||
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public async Task CreateStoreAsync()
|
||||
{
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
|
||||
{
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
|
||||
}
|
||||
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() + (segwit ? "" : "-[legacy]"));
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
var account = parent.PayTester.GetController<AccountController>();
|
||||
await account.Register(new RegisterViewModel()
|
||||
{
|
||||
Email = Guid.NewGuid() + "@toto.com",
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
});
|
||||
UserId = account.RegisteredUserId;
|
||||
}
|
||||
|
||||
public Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = connectionString,
|
||||
SkipPortTest = true
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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>();
|
||||
}
|
||||
}
|
||||
}
|
@ -10,54 +10,54 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class Utils
|
||||
{
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/a/14933880/2061103
|
||||
public static void DeleteDirectory(string destinationDir)
|
||||
{
|
||||
const int magicDust = 10;
|
||||
for(var gnomes = 1; gnomes <= magicDust; gnomes++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(destinationDir, true);
|
||||
}
|
||||
catch(DirectoryNotFoundException)
|
||||
{
|
||||
return; // good!
|
||||
}
|
||||
catch(IOException)
|
||||
{
|
||||
if(gnomes == magicDust)
|
||||
throw;
|
||||
// System.IO.IOException: The directory is not empty
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
// http://stackoverflow.com/a/14933880/2061103
|
||||
public static void DeleteDirectory(string destinationDir)
|
||||
{
|
||||
const int magicDust = 10;
|
||||
for (var gnomes = 1; gnomes <= magicDust; gnomes++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(destinationDir, true);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return; // good!
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
if (gnomes == magicDust)
|
||||
throw;
|
||||
// System.IO.IOException: The directory is not empty
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
catch(UnauthorizedAccessException)
|
||||
{
|
||||
if(gnomes == magicDust)
|
||||
throw;
|
||||
// Wait, maybe another software make us authorized a little later
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
if (gnomes == magicDust)
|
||||
throw;
|
||||
// Wait, maybe another software make us authorized a little later
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// depending on your use case, consider throwing an exception here
|
||||
}
|
||||
}
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// depending on your use case, consider throwing an exception here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
BTCPayServer.Tests/docker-bitcoin-cli.ps1
Normal file
2
BTCPayServer.Tests/docker-bitcoin-cli.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
$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
|
4
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
4
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
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" "$@"
|
@ -1,5 +1,8 @@
|
||||
version: "3"
|
||||
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.
|
||||
services:
|
||||
|
||||
tests:
|
||||
@ -7,51 +10,282 @@ services:
|
||||
context: ..
|
||||
dockerfile: BTCPayServer.Tests/Dockerfile
|
||||
environment:
|
||||
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_NBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_FAKECALLBACK: 'true'
|
||||
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
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"
|
||||
links:
|
||||
- nbxplorer
|
||||
- dev
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
volumes:
|
||||
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
|
||||
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.18
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
NBXPLORER_RPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_RPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_RPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_NODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_CHAINS: "btc,ltc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_LTCRPCURL: http://litecoind:43782/
|
||||
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
|
||||
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
- bitcoind
|
||||
- postgres
|
||||
- litecoind
|
||||
|
||||
|
||||
bitcoind:
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: "regtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
|
||||
BITCOIN_RPC_USER: ceiwHEbqWI83
|
||||
BITCOIN_RPC_PASSWORD: DwubwWsoo3
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
rpcport=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"
|
||||
expose:
|
||||
- "43782"
|
||||
- "39388"
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "28332" # ZMQ
|
||||
- "28333" # ZMQ
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "customer_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.6-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
links:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=merchant_lightningd
|
||||
network=regtest
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
ports:
|
||||
- "30993:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
restart: unless-stopped
|
||||
image: nicolasdorier/docker-litecoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
ports:
|
||||
- "43783:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
|
||||
postgres:
|
||||
image: postgres:9.6.5
|
||||
ports:
|
||||
- "39372:5432"
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.12
|
||||
expose:
|
||||
- "3306"
|
||||
ports:
|
||||
- "33036:3306"
|
||||
environment:
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.5.1-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
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.5.1-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
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
customer_lnd_datadir:
|
||||
merchant_lnd_datadir:
|
||||
|
2
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
2
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
$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
|
4
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
4
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
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 "$@"
|
5
BTCPayServer.Tests/docker-entrypoint.sh
Executable file
5
BTCPayServer.Tests/docker-entrypoint.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
dotnet test --filter Fast=Fast --no-build
|
||||
dotnet test --filter Integration=Integration --no-build -v n
|
2
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
2
BTCPayServer.Tests/docker-litecoin-cli.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
$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
|
4
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
4
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
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" "$@"
|
2
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
2
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
@ -0,0 +1,2 @@
|
||||
$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
|
4
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
4
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
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
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitIdentity : IIdentity
|
||||
{
|
||||
public BitIdentity(PubKey key)
|
||||
{
|
||||
PubKey = key;
|
||||
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
|
||||
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
|
||||
}
|
||||
string _Name;
|
||||
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
@ -8,43 +8,43 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitTokenEntity
|
||||
{
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset PairingTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset PairingTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public BitTokenEntity Clone(Facade facade)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = Label,
|
||||
Facade = Facade,
|
||||
StoreId = StoreId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
Value = Value
|
||||
};
|
||||
}
|
||||
}
|
||||
public BitTokenEntity Clone(Facade facade)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = Label,
|
||||
Facade = Facade,
|
||||
StoreId = StoreId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
Value = Value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,47 +4,47 @@ using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class PairingCodeEntity
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset CreatedTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset Expiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TokenValue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public class PairingCodeEntity
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset CreatedTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset Expiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TokenValue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTimeOffset.UtcNow > Expiration;
|
||||
}
|
||||
}
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTimeOffset.UtcNow > Expiration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,173 +13,240 @@ using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class TokenRepository
|
||||
{
|
||||
ApplicationDbContextFactory _Factory;
|
||||
public TokenRepository(ApplicationDbContextFactory dbFactory)
|
||||
{
|
||||
if(dbFactory == null)
|
||||
throw new ArgumentNullException(nameof(dbFactory));
|
||||
_Factory = dbFactory;
|
||||
}
|
||||
public enum PairingResult
|
||||
{
|
||||
Partial,
|
||||
Complete,
|
||||
ReusedKey,
|
||||
Expired
|
||||
}
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
.Where(p => p.SIN == sin)
|
||||
.ToListAsync())
|
||||
.Select(p => CreateTokenEntity(p))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
public class TokenRepository
|
||||
{
|
||||
ApplicationDbContextFactory _Factory;
|
||||
public TokenRepository(ApplicationDbContextFactory dbFactory)
|
||||
{
|
||||
if (dbFactory == null)
|
||||
throw new ArgumentNullException(nameof(dbFactory));
|
||||
_Factory = dbFactory;
|
||||
}
|
||||
|
||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = data.Label,
|
||||
Facade = data.Facade,
|
||||
Value = data.Id,
|
||||
SIN = data.SIN,
|
||||
PairingTime = data.PairingTime,
|
||||
StoreId = data.StoreDataId
|
||||
};
|
||||
}
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
if (sin == null)
|
||||
return Array.Empty<BitTokenEntity>();
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
.Where(p => p.SIN == sin)
|
||||
.ToListAsync())
|
||||
.Select(p => CreateTokenEntity(p))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreatePairingCodeAsync()
|
||||
{
|
||||
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
|
||||
await ctx.PairingCodes.AddAsync(new PairingCodeData()
|
||||
{
|
||||
Id = pairingCodeId,
|
||||
DateCreated = now,
|
||||
Expiration = expiration,
|
||||
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return pairingCodeId;
|
||||
}
|
||||
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
}
|
||||
public async Task GenerateLegacyAPIKey(string storeId)
|
||||
{
|
||||
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
|
||||
// as good as possible. The string below got generated for me.
|
||||
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
|
||||
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
|
||||
var generated = new char[chars.Length];
|
||||
for (int i = 0; i < generated.Length; i++)
|
||||
{
|
||||
generated[i] = chars[rand.Next(0, generated.Length)];
|
||||
}
|
||||
|
||||
public async Task<bool> PairWithStoreAsync(string pairingCodeId, string storeId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return false;
|
||||
pairingCode.StoreDataId = storeId;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PairWithSINAsync(string pairingCodeId, string sin)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return false;
|
||||
pairingCode.SIN = sin;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = data.Label,
|
||||
Facade = data.Facade,
|
||||
Value = data.Id,
|
||||
SIN = data.SIN,
|
||||
PairingTime = data.PairingTime,
|
||||
StoreId = data.StoreDataId
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string> CreatePairingCodeAsync()
|
||||
{
|
||||
string pairingCodeId = null;
|
||||
while (true)
|
||||
{
|
||||
pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
if (pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits
|
||||
break;
|
||||
}
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
|
||||
ctx.PairingCodes.Add(new PairingCodeData()
|
||||
{
|
||||
Id = pairingCodeId,
|
||||
DateCreated = now,
|
||||
Expiration = expiration,
|
||||
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return pairingCodeId;
|
||||
}
|
||||
|
||||
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return PairingResult.Expired;
|
||||
pairingCode.StoreDataId = storeId;
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return PairingResult.Expired;
|
||||
pairingCode.SIN = sin;
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
|
||||
{
|
||||
ctx.PairingCodes.Remove(pairingCode);
|
||||
await ctx.PairedSINData.AddAsync(new PairedSINData()
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
});
|
||||
}
|
||||
}
|
||||
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
|
||||
{
|
||||
ctx.PairingCodes.Remove(pairingCode);
|
||||
|
||||
// Can have concurrency issues... but no harm can be done
|
||||
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
|
||||
if (alreadyUsed)
|
||||
return PairingResult.ReusedKey;
|
||||
await ctx.PairedSINData.AddAsync(new PairedSINData()
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
});
|
||||
return PairingResult.Complete;
|
||||
}
|
||||
return PairingResult.Partial;
|
||||
}
|
||||
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
|
||||
.Select(c => CreateTokenEntity(c))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
|
||||
.Select(c => CreateTokenEntity(c))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
|
||||
}
|
||||
}
|
||||
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
|
||||
}
|
||||
}
|
||||
|
||||
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
|
||||
{
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
Id = data.Id,
|
||||
Label = data.Label,
|
||||
Expiration = data.Expiration,
|
||||
CreatedTime = data.DateCreated,
|
||||
TokenValue = data.TokenValue,
|
||||
SIN = data.SIN
|
||||
};
|
||||
}
|
||||
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
|
||||
{
|
||||
if (data == null)
|
||||
return null;
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
Id = data.Id,
|
||||
Label = data.Label,
|
||||
Expiration = data.Expiration,
|
||||
CreatedTime = data.DateCreated,
|
||||
TokenValue = data.TokenValue,
|
||||
SIN = data.SIN
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> DeleteToken(string tokenId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if(token == null)
|
||||
return false;
|
||||
ctx.PairedSINData.Remove(token);
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public async Task<bool> DeleteToken(string tokenId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if (token == null)
|
||||
return false;
|
||||
ctx.PairedSINData.Remove(token);
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BitTokenEntity> GetToken(string tokenId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
public async Task<BitTokenEntity> GetToken(string tokenId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if (token == null)
|
||||
return null;
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
79
BTCPayServer/BTCPayNetwork.cs
Normal file
79
BTCPayServer/BTCPayNetwork.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
|
||||
{
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||
chainType == NetworkType.Regtest ? 23002 :
|
||||
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
{
|
||||
public Network NBitcoinNetwork { get; set; }
|
||||
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")]
|
||||
public bool IsBTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return CryptoCode == "BTC";
|
||||
}
|
||||
}
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public string LightningImagePath { get; set; }
|
||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
}
|
||||
|
||||
internal KeyPath GetRootKeyPath()
|
||||
{
|
||||
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
|
||||
.Derive(CoinType);
|
||||
}
|
||||
}
|
||||
}
|
32
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
32
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinGold()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "BGold",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoingold",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitDash()
|
||||
{
|
||||
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dash",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://insight.dash.org/insight/tx/{0}"
|
||||
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dash",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DASH_X = DASH_BTC * BTC_X",
|
||||
"DASH_BTC = bittrex(DASH_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dash.png",
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Dogecoin.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 InitDogecoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dogecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DOGE_X = DOGE_BTC * BTC_X",
|
||||
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.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 InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Feathercoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "feathercoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FTC_X = FTC_BTC * BTC_X",
|
||||
"FTC_BTC = bittrex(FTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/feathercoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
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",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
31
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
31
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
@ -0,0 +1,31 @@
|
||||
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 InitLitecoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.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 InitMonacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Monacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "monacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = bittrex(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Polis.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 InitPolis()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Polis",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "polis",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"POLIS_X = POLIS_BTC * BTC_X",
|
||||
"POLIS_BTC = cryptopia(POLIS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/polis.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Ufo.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 InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Ufo",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "ufo",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"UFO_X = UFO_BTC * BTC_X",
|
||||
"UFO_BTC = coinexchange(UFO_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/ufo.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.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 InitViacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Viacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "viacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"VIA_X = VIA_BTC * BTC_X",
|
||||
"VIA_BTC = bittrex(VIA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/viacoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
109
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
109
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
|
||||
|
||||
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return _NBXplorerNetworkProvider;
|
||||
}
|
||||
}
|
||||
|
||||
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||
{
|
||||
NetworkType = filtered.NetworkType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
|
||||
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||
foreach (var network in filtered._Networks)
|
||||
{
|
||||
if(cryptoCodes.Contains(network.Key))
|
||||
{
|
||||
_Networks.Add(network.Key, network.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkType NetworkType { get; private set; }
|
||||
public BTCPayNetworkProvider(NetworkType networkType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitBitcore();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keep only the specified crypto
|
||||
/// </summary>
|
||||
/// <param name="cryptoCodes">Crypto to support</param>
|
||||
/// <returns></returns>
|
||||
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
|
||||
{
|
||||
return new BTCPayNetworkProvider(this, cryptoCodes);
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
public BTCPayNetwork BTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(BTCPayNetwork network)
|
||||
{
|
||||
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
|
||||
}
|
||||
|
||||
public IEnumerable<BTCPayNetwork> GetAll()
|
||||
{
|
||||
return _Networks.Values.ToArray();
|
||||
}
|
||||
|
||||
public bool Support(string cryptoCode)
|
||||
{
|
||||
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
|
||||
{
|
||||
if (cryptoCode == "XBT")
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.9</Version>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.53</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\css\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Remove="Build\dockerfiles\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="wwwroot\css\**" />
|
||||
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<EmbeddedResource Remove="Build\dockerfiles\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="wwwroot\css\**" />
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\dockerfiles\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="wwwroot\css\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
@ -17,43 +33,51 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<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.1.78" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
|
||||
<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.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.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\img\bitcoin-symbol.svg" />
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
<None Include="wwwroot\js\site.js" />
|
||||
<None Include="wwwroot\js\site.min.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
|
||||
<None Include="wwwroot\checkout\js\core.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
@ -99,5 +123,69 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Apps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Home\BitpayTranslator.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LightningChargeServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SparkServices.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>
|
||||
<Content Update="Views\Stores\PayButton.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Public\PayButtonHandle.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Maintenance.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Services.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\ListWallets.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>
|
||||
<Content Update="Views\Wallets\_Nav.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\_ViewImports.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\_ViewStart.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
72
BTCPayServer/BTCPayServer.ruleset
Normal file
72
BTCPayServer/BTCPayServer.ruleset
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
|
||||
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
|
||||
<Name Resource="MinimumRecommendedRules_Name" />
|
||||
<Description Resource="MinimumRecommendedRules_Description" />
|
||||
</Localization>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1009" Action="Warning" />
|
||||
<Rule Id="CA1016" Action="Warning" />
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1049" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1061" Action="Warning" />
|
||||
<Rule Id="CA1063" Action="Warning" />
|
||||
<Rule Id="CA1065" Action="Warning" />
|
||||
<Rule Id="CA1301" Action="Warning" />
|
||||
<Rule Id="CA1400" Action="Warning" />
|
||||
<Rule Id="CA1401" Action="Warning" />
|
||||
<Rule Id="CA1403" Action="Warning" />
|
||||
<Rule Id="CA1404" Action="Warning" />
|
||||
<Rule Id="CA1405" Action="Warning" />
|
||||
<Rule Id="CA1410" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1821" Action="Warning" />
|
||||
<Rule Id="CA1900" Action="Warning" />
|
||||
<Rule Id="CA1901" Action="Warning" />
|
||||
<Rule Id="CA2002" Action="Warning" />
|
||||
<Rule Id="CA2100" Action="Warning" />
|
||||
<Rule Id="CA2101" Action="Warning" />
|
||||
<Rule Id="CA2108" Action="Warning" />
|
||||
<Rule Id="CA2111" Action="Warning" />
|
||||
<Rule Id="CA2112" Action="Warning" />
|
||||
<Rule Id="CA2114" Action="Warning" />
|
||||
<Rule Id="CA2116" Action="Warning" />
|
||||
<Rule Id="CA2117" Action="Warning" />
|
||||
<Rule Id="CA2122" Action="Warning" />
|
||||
<Rule Id="CA2123" Action="Warning" />
|
||||
<Rule Id="CA2124" Action="Warning" />
|
||||
<Rule Id="CA2126" Action="Warning" />
|
||||
<Rule Id="CA2131" Action="Warning" />
|
||||
<Rule Id="CA2132" Action="Warning" />
|
||||
<Rule Id="CA2133" Action="Warning" />
|
||||
<Rule Id="CA2134" Action="Warning" />
|
||||
<Rule Id="CA2137" Action="Warning" />
|
||||
<Rule Id="CA2138" Action="Warning" />
|
||||
<Rule Id="CA2140" Action="Warning" />
|
||||
<Rule Id="CA2141" Action="Warning" />
|
||||
<Rule Id="CA2146" Action="Warning" />
|
||||
<Rule Id="CA2147" Action="Warning" />
|
||||
<Rule Id="CA2149" Action="Warning" />
|
||||
<Rule Id="CA2200" Action="Warning" />
|
||||
<Rule Id="CA2202" Action="Warning" />
|
||||
<Rule Id="CA2207" Action="Warning" />
|
||||
<Rule Id="CA2212" Action="Warning" />
|
||||
<Rule Id="CA2213" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="Warning" />
|
||||
<Rule Id="CA2216" Action="Warning" />
|
||||
<Rule Id="CA2220" Action="Warning" />
|
||||
<Rule Id="CA2229" Action="Warning" />
|
||||
<Rule Id="CA2231" Action="Warning" />
|
||||
<Rule Id="CA2232" Action="Warning" />
|
||||
<Rule Id="CA2235" Action="Warning" />
|
||||
<Rule Id="CA2236" Action="Warning" />
|
||||
<Rule Id="CA2237" Action="Warning" />
|
||||
<Rule Id="CA2238" Action="Warning" />
|
||||
<Rule Id="CA2240" Action="Warning" />
|
||||
<Rule Id="CA2241" Action="Warning" />
|
||||
<Rule Id="CA2242" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
|
||||
</RuleSet>
|
@ -4,15 +4,15 @@ using System.Text;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BitpayHttpException : Exception
|
||||
{
|
||||
public BitpayHttpException(int code, string message) : base(message)
|
||||
{
|
||||
StatusCode = code;
|
||||
}
|
||||
public int StatusCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class BitpayHttpException : Exception
|
||||
{
|
||||
public BitpayHttpException(int code, string message) : base(message)
|
||||
{
|
||||
StatusCode = code;
|
||||
}
|
||||
public int StatusCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,64 +9,332 @@ using System.Net;
|
||||
using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri Explorer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public class NBXplorerConnectionSetting
|
||||
{
|
||||
public string CryptoCode { get; internal set; }
|
||||
public Uri ExplorerUri { get; internal set; }
|
||||
public string CookieFile { get; internal set; }
|
||||
}
|
||||
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ConfigurationFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DataDir
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public NetworkType NetworkType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ConfigurationFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
var networkInfo = DefaultConfiguration.GetNetwork(conf);
|
||||
Network = networkInfo?.Network;
|
||||
if(Network == null)
|
||||
throw new ConfigException("Invalid network");
|
||||
public string LogFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DataDir
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + Network);
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
|
||||
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
|
||||
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
}
|
||||
public bool DisableRegistration
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool RequireHttps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
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);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
{
|
||||
if (NetworkProvider.GetNetwork(chain) == null)
|
||||
throw new ConfigException($"Invalid chains \"{chain}\"");
|
||||
}
|
||||
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in NetworkProvider.GetAll())
|
||||
{
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
|
||||
if (spark.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(spark, out var connectionString))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
|
||||
}
|
||||
|
||||
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
|
||||
if (charge.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
|
||||
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
|
||||
|
||||
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + 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 +
|
||||
chargeError ?? string.Empty);
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
|
||||
}
|
||||
}
|
||||
|
||||
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))))
|
||||
{
|
||||
ExternalServices.AddOrReplace(service.Name, service.Link);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
{
|
||||
int waitTime = 0;
|
||||
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
|
||||
{
|
||||
if (waitTime++ < 5)
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
else
|
||||
throw new ConfigException($"sshkeyfile does not exist");
|
||||
}
|
||||
|
||||
if (sshSettings.Port > ushort.MaxValue ||
|
||||
sshSettings.Port < ushort.MinValue)
|
||||
throw new ConfigException($"ssh port is invalid");
|
||||
if (!string.IsNullOrEmpty(sshSettings.Password) && !string.IsNullOrEmpty(sshSettings.KeyFile))
|
||||
throw new ConfigException($"sshpassword or sshkeyfile should be provided, but not both");
|
||||
try
|
||||
{
|
||||
sshSettings.CreateConnectionInfo();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ConfigException($"sshkeyfilepassword is invalid");
|
||||
}
|
||||
SSHSettings = sshSettings;
|
||||
}
|
||||
|
||||
var fingerPrints = conf.GetOrDefault<string>("sshtrustedfingerprints", "");
|
||||
if (!string.IsNullOrEmpty(fingerPrints))
|
||||
{
|
||||
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!SSHFingerprint.TryParse(fingerprint, out var f))
|
||||
throw new ConfigException($"Invalid ssh fingerprint format {fingerprint}");
|
||||
TrustedFingerprints.Add(f);
|
||||
}
|
||||
}
|
||||
|
||||
RootPath = conf.GetOrDefault<string>("rootpath", "/");
|
||||
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
RootPath = "/" + RootPath;
|
||||
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
if (old != null)
|
||||
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)
|
||||
{
|
||||
var parts = settings.Server.Split(':');
|
||||
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
|
||||
{
|
||||
settings.Port = port;
|
||||
settings.Server = parts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Port = 22;
|
||||
}
|
||||
|
||||
parts = settings.Server.Split('@');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
settings.Username = parts[0];
|
||||
settings.Server = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
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", "");
|
||||
return settings;
|
||||
}
|
||||
|
||||
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
|
||||
{
|
||||
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
|
||||
}
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string MySQLConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerRuntime : IDisposable
|
||||
{
|
||||
public ExplorerClient Explorer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void Configure(BTCPayServerOptions opts)
|
||||
{
|
||||
ConfigureAsync(opts).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task ConfigureAsync(BTCPayServerOptions opts)
|
||||
{
|
||||
Network = opts.Network;
|
||||
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
|
||||
|
||||
if(!Explorer.SetCookieAuth(opts.CookieFile))
|
||||
Explorer.SetNoAuth();
|
||||
|
||||
CancellationTokenSource cts = new CancellationTokenSource(30000);
|
||||
try
|
||||
{
|
||||
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
|
||||
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
|
||||
Logs.Configuration.LogInformation("Connection successfull");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
|
||||
}
|
||||
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
|
||||
_Resources.Add(db);
|
||||
|
||||
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
|
||||
_Resources.Add(db);
|
||||
|
||||
ApplicationDbContextFactory dbContext = null;
|
||||
if(opts.PostgresConnectionString == null)
|
||||
{
|
||||
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
|
||||
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
DBFactory = dbContext;
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
|
||||
}
|
||||
|
||||
private static string CreateDBPath(BTCPayServerOptions opts, string name)
|
||||
{
|
||||
var dbpath = Path.Combine(opts.DataDir, name);
|
||||
if(!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return dbpath;
|
||||
}
|
||||
|
||||
List<IDisposable> _Resources = new List<IDisposable>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock(_Resources)
|
||||
{
|
||||
foreach(var r in _Resources)
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
_Resources.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ApplicationDbContextFactory DBFactory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -5,11 +5,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ConfigException : Exception
|
||||
{
|
||||
public ConfigException(string message) : base(message)
|
||||
{
|
||||
public class ConfigException : Exception
|
||||
{
|
||||
public ConfigException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,44 +9,53 @@ using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
|
||||
if(str == null)
|
||||
return defaultValue;
|
||||
if(typeof(T) == typeof(bool))
|
||||
{
|
||||
var trueValues = new[] { "1", "true" };
|
||||
var falseValues = new[] { "0", "false" };
|
||||
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)true;
|
||||
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)false;
|
||||
throw new FormatException();
|
||||
}
|
||||
else if(typeof(T) == typeof(Uri))
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
else if(typeof(T) == typeof(string))
|
||||
return (T)(object)str;
|
||||
else if(typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":");
|
||||
if(separator == -1)
|
||||
throw new FormatException();
|
||||
var ip = str.Substring(0, separator);
|
||||
var port = str.Substring(separator + 1);
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
|
||||
}
|
||||
else if(typeof(T) == typeof(int))
|
||||
{
|
||||
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
|
||||
if (str == null)
|
||||
return defaultValue;
|
||||
if (typeof(T) == typeof(bool))
|
||||
{
|
||||
var trueValues = new[] { "1", "true" };
|
||||
var falseValues = new[] { "0", "false" };
|
||||
if (trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)true;
|
||||
if (falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)false;
|
||||
throw new FormatException();
|
||||
}
|
||||
else if (typeof(T) == typeof(Uri))
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
}
|
||||
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);
|
||||
if (separator == -1)
|
||||
throw new FormatException();
|
||||
var ip = str.Substring(0, separator);
|
||||
var port = str.Substring(separator + 1);
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (typeof(T) == typeof(int))
|
||||
{
|
||||
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,91 +10,130 @@ using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using System.Text;
|
||||
using CommandLine;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
|
||||
Name = "NBXplorer"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
|
||||
app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
|
||||
Name = "BTCPay"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
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);
|
||||
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
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("--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();
|
||||
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
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;
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
public override string EnvironmentVariablePrefix => "BTCPAY_";
|
||||
|
||||
public override string EnvironmentVariablePrefix => "BTCPAY_";
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return GetNetwork(conf).DefaultDataDirectory;
|
||||
}
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
|
||||
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
|
||||
chainDir = Path.Combine(dataDir, chainDir);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(chainDir))
|
||||
Directory.CreateDirectory(chainDir);
|
||||
}
|
||||
catch { }
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
var dataDir = conf["datadir"];
|
||||
if(dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
|
||||
return Path.Combine(dataDir, fileName);
|
||||
}
|
||||
public static NetworkType GetNetworkType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
{
|
||||
var n = Network.GetNetwork(network);
|
||||
if (n == null)
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.NetworkType;
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
|
||||
|
||||
public static NetworkInformation GetNetwork(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if(network != null)
|
||||
{
|
||||
var info = NetworkInformation.GetNetworkByName(network);
|
||||
if(info == null)
|
||||
throw new ConfigException($"Invalid network name {network}");
|
||||
return info;
|
||||
}
|
||||
return net;
|
||||
}
|
||||
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
|
||||
|
||||
return NetworkInformation.GetNetworkByName(net.Name);
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#testnet=0");
|
||||
builder.AppendLine("#regtest=0");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#requirehttps=0");
|
||||
builder.AppendLine("#port=" + network.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
|
||||
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
|
||||
return builder.ToString();
|
||||
}
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var networkType = GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
builder.AppendLine();
|
||||
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;");
|
||||
builder.AppendLine("#mysql=User ID=root;Password=myPassword;Host=localhost;Port=3306;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
|
||||
}
|
||||
}
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
BTCPayServer/Configuration/External/ExternalCharge.cs
vendored
Normal file
19
BTCPayServer/Configuration/External/ExternalCharge.cs
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalCharge : ExternalService
|
||||
{
|
||||
public ExternalCharge(LightningConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
public LightningConnectionString ConnectionString { get; }
|
||||
}
|
||||
}
|
30
BTCPayServer/Configuration/External/ExternalLnd.cs
vendored
Normal file
30
BTCPayServer/Configuration/External/ExternalLnd.cs
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
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, string type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
|
||||
}
|
||||
}
|
22
BTCPayServer/Configuration/External/ExternalService.cs
vendored
Normal file
22
BTCPayServer/Configuration/External/ExternalService.cs
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
19
BTCPayServer/Configuration/External/ExternalSpark.cs
vendored
Normal file
19
BTCPayServer/Configuration/External/ExternalSpark.cs
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalSpark : ExternalService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalSpark(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class NetworkInformation
|
||||
{
|
||||
static NetworkInformation()
|
||||
{
|
||||
_Networks = new Dictionary<string, NetworkInformation>();
|
||||
foreach(var network in Network.GetNetworks())
|
||||
{
|
||||
NetworkInformation info = new NetworkInformation();
|
||||
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
|
||||
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
|
||||
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
|
||||
info.Network = network;
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
|
||||
info.DefaultPort = 23002;
|
||||
_Networks.Add(network.Name, info);
|
||||
if(network == Network.Main)
|
||||
{
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
|
||||
Main = info;
|
||||
info.DefaultPort = 23000;
|
||||
}
|
||||
if(network == Network.TestNet)
|
||||
{
|
||||
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
|
||||
info.DefaultPort = 23001;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, NetworkInformation> _Networks;
|
||||
public static NetworkInformation GetNetworkByName(string name)
|
||||
{
|
||||
var value = _Networks.TryGet(name);
|
||||
if(value != null)
|
||||
return value;
|
||||
|
||||
//Maybe alias ?
|
||||
var network = Network.GetNetwork(name);
|
||||
if(network != null)
|
||||
{
|
||||
value = _Networks.TryGet(network.Name);
|
||||
if(value != null)
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static NetworkInformation Main
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DefaultConfigurationFile
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DefaultDataDirectory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri DefaultExplorerUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public int DefaultPort
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DefaultExplorerCookieFile
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Network.ToString();
|
||||
}
|
||||
|
||||
public static string ToStringAll()
|
||||
{
|
||||
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
47
BTCPayServer/Configuration/SparkConnectionString.cs
Normal file
47
BTCPayServer/Configuration/SparkConnectionString.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class SparkConnectionString
|
||||
{
|
||||
public Uri Server { get; private set; }
|
||||
public string CookeFile { get; private set; }
|
||||
|
||||
public static bool TryParse(string str, out SparkConnectionString result)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
result = null;
|
||||
var resultTemp = new SparkConnectionString();
|
||||
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)
|
||||
return false;
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
|
||||
return false;
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookeFile != null)
|
||||
return false;
|
||||
resultTemp.CookeFile = kv[1];
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -12,69 +13,84 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
public AccessTokenController(TokenRepository tokenRepository)
|
||||
{
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
public AccessTokenController(TokenRepository tokenRepository)
|
||||
{
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
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");
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
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
|
||||
});
|
||||
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
|
||||
});
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
|
||||
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
pairingEntity.SIN = sin;
|
||||
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
|
||||
throw new BitpayHttpException(400, "Unknown pairing code");
|
||||
|
||||
}
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
if (pairingEntity == null)
|
||||
throw new BitpayHttpException(404, "The specified pairingCode is not found");
|
||||
pairingEntity.SIN = sin;
|
||||
|
||||
var pairingCodes = new List<PairingCodeResponse>
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
Facade = pairingEntity.Facade,
|
||||
Token = pairingEntity.TokenValue,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
};
|
||||
return DataWrapper.Create(pairingCodes);
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label))
|
||||
{
|
||||
pairingEntity.Label = request.Label;
|
||||
await _TokenRepository.UpdatePairingCode(pairingEntity);
|
||||
}
|
||||
|
||||
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
|
||||
if (result != PairingResult.Complete && result != PairingResult.Partial)
|
||||
throw new BitpayHttpException(400, $"Error while pairing ({result})");
|
||||
|
||||
}
|
||||
|
||||
var pairingCodes = new List<PairingCodeResponse>
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
Policies = new Newtonsoft.Json.Linq.JArray(),
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
Facade = pairingEntity.Facade,
|
||||
Token = pairingEntity.TokenValue,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
};
|
||||
return DataWrapper.Create(pairingCodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,506 +15,527 @@ using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger _logger;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository,
|
||||
ILogger<AccountController> logger)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
}
|
||||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string ErrorMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[TempData]
|
||||
public string ErrorMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
// Require the user to have a confirmed email before they can log on.
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if(user != null)
|
||||
{
|
||||
if(user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"You must have a confirmed email to log in.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if(result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(LoginWith2fa), new
|
||||
{
|
||||
returnUrl,
|
||||
model.RememberMe
|
||||
});
|
||||
}
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Require the user to have a confirmed email before they can log on.
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user != null)
|
||||
{
|
||||
if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"You must have a confirmed email to log in.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(LoginWith2fa), new
|
||||
{
|
||||
returnUrl,
|
||||
model.RememberMe
|
||||
});
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View();
|
||||
}
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Lockout()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Lockout()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register(string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, bool logon = true)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
|
||||
if (admin.Count == 0)
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if(admin.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Admin created.");
|
||||
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);
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
if(!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["StatusMessage"] = "Account created, please confirm your email";
|
||||
return View();
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
if(logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["StatusMessage"] = "Account created, please confirm your email";
|
||||
return View();
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User logged out.");
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult ExternalLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
|
||||
{
|
||||
returnUrl
|
||||
});
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User logged out.");
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
|
||||
{
|
||||
if(remoteError != null)
|
||||
{
|
||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if(info == null)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult ExternalLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new
|
||||
{
|
||||
returnUrl
|
||||
});
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have an account, then ask the user to create an account.
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["LoginProvider"] = info.LoginProvider;
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
|
||||
}
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
|
||||
{
|
||||
if (remoteError != null)
|
||||
{
|
||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if(info == null)
|
||||
{
|
||||
throw new ApplicationException("Error loading external login information during confirmation.");
|
||||
}
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have an account, then ask the user to create an account.
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["LoginProvider"] = info.LoginProvider;
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
|
||||
}
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(nameof(ExternalLogin), model);
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
throw new ApplicationException("Error loading external login information during confirmation.");
|
||||
}
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ConfirmEmail(string userId, string code)
|
||||
{
|
||||
if(userId == null || code == null)
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
return View(result.Succeeded ? "ConfirmEmail" : "Error");
|
||||
}
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(nameof(ExternalLogin), model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPassword()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ConfirmEmail(string userId, string code)
|
||||
{
|
||||
if (userId == null || code == null)
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
return View(result.Succeeded ? "ConfirmEmail" : "Error");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if(user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPassword()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please
|
||||
// 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>");
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
// For more information on how to enable account confirmation and password reset please
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
||||
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPassword(string code = null)
|
||||
{
|
||||
if(code == null)
|
||||
{
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
}
|
||||
var model = new ResetPasswordViewModel { Code = code };
|
||||
return View(model);
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if(user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||
if(result.Succeeded)
|
||||
{
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
AddErrors(result);
|
||||
return View();
|
||||
}
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPassword(string code = null)
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
}
|
||||
var model = new ResetPasswordViewModel { Code = code };
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
AddErrors(result);
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
{
|
||||
foreach(var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
private void AddErrors(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl)
|
||||
{
|
||||
if(Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
}
|
||||
private IActionResult RedirectToLocal(string returnUrl)
|
||||
{
|
||||
if (Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class CrowdfundAppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public CrowdfundSettings Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
}
|
||||
|
||||
public class CrowdfundSettings
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public string TargetCurrency { get; set; }
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string MainImageUrl { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string PerksTemplate { get; set; }
|
||||
public bool DisqusEnabled { get; set; }= false;
|
||||
public bool SoundsEnabled { get; set; }= true;
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
public bool UseInvoiceAmount { get; set; } = true;
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[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()
|
||||
{
|
||||
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,
|
||||
UseInvoiceAmount = settings.UseInvoiceAmount,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = settings.UseAllStoreInvoices,
|
||||
AppId = appId,
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
_AppsHelper.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.");
|
||||
}
|
||||
|
||||
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 = _AppsHelper.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
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),
|
||||
UseInvoiceAmount = vm.UseInvoiceAmount,
|
||||
UseAllStoreInvoices = vm.UseAllStoreInvoices,
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
};
|
||||
|
||||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
|
||||
}
|
||||
}
|
||||
}
|
203
BTCPayServer/Controllers/AppsController.PointOfSale.cs
Normal file
203
BTCPayServer/Controllers/AppsController.PointOfSale.cs
Normal file
@ -0,0 +1,203 @@
|
||||
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 Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class PointOfSaleSettings
|
||||
{
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "Tea shop";
|
||||
Currency = "USD";
|
||||
Template =
|
||||
"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;
|
||||
}
|
||||
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 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; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Id = appId,
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Currency = settings.Currency,
|
||||
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
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
|
||||
var encoder = HtmlEncoder.Default;
|
||||
if (settings.ShowCustomAmount)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example1 = builder.ToString();
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = _AppsHelper.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\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example2 = builder.ToString();
|
||||
}
|
||||
catch { }
|
||||
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
|
||||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
_AppsHelper.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(app);
|
||||
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
||||
ctx.Entry<AppData>(app).Property(a => a.Settings).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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
172
BTCPayServer/Controllers/AppsController.cs
Normal file
172
BTCPayServer/Controllers/AppsController.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class AppsController : Controller
|
||||
{
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await _AppsHelper.GetAllApps(GetUserId());
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteAppPost(string appId)
|
||||
{
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await _AppsHelper.DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created 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();
|
||||
vm.SetStores(stores);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created 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;
|
||||
vm.SetStores(stores);
|
||||
vm.SelectedStore = selectedStore;
|
||||
|
||||
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType))
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (!stores.Any(s => s.Id == selectedStore))
|
||||
{
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var appData = new AppData() { Id = id };
|
||||
appData.StoreDataId = selectedStore;
|
||||
appData.Name = vm.Name;
|
||||
appData.AppType = appType.ToString();
|
||||
ctx.Apps.Add(appData);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
CreatedAppId = id;
|
||||
|
||||
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]
|
||||
[Route("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteApp(string appId)
|
||||
{
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Delete app {appData.Name} ({appData.AppType})",
|
||||
Description = "This app will be removed from this store",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
{
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
499
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
499
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
@ -0,0 +1,499 @@
|
||||
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.Tasks;
|
||||
using BTCPayServer.Crowdfund;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper,
|
||||
InvoiceController invoiceController,
|
||||
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
_CrowdfundHubStreamer = crowdfundHubStreamer;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
|
||||
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);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppsHelper.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,
|
||||
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 = _AppsHelper.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 _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppsHelper.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.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
if(!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = await _CrowdfundHubStreamer.GetCrowdfundInfo(appId);
|
||||
|
||||
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 _AppsHelper.GetStore(app);
|
||||
var title = settings.Title;
|
||||
var price = request.Amount;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _AppsHelper.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 Invoice()
|
||||
{
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
|
||||
|
||||
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
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,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey,
|
||||
string posData = null)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
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);
|
||||
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 && !settings.EnableShoppingCart)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await _AppsHelper.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
private HtmlSanitizer _HtmlSanitizer;
|
||||
public CurrencyNameTable Currencies => _Currencies;
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
ConfigureSanitizer();
|
||||
}
|
||||
|
||||
private void ConfigureSanitizer()
|
||||
{
|
||||
|
||||
_HtmlSanitizer = new HtmlSanitizer();
|
||||
|
||||
|
||||
_HtmlSanitizer.RemovingAtRule += (sender, args) =>
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
|
||||
};
|
||||
_HtmlSanitizer.RemovingTag += (sender, args) =>
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (!args.Tag.ClassList.Contains("img-fluid"))
|
||||
{
|
||||
args.Tag.ClassList.Add("img-fluid");
|
||||
}
|
||||
|
||||
args.Cancel = true;
|
||||
}
|
||||
};
|
||||
|
||||
_HtmlSanitizer.RemovingAttribute += (sender, args) =>
|
||||
{
|
||||
if (args.Tag.TagName.Equals("img",StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Attribute.Name.Equals( "src", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Reason == RemoveReason.NotAllowedUrlValue)
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
Debug.WriteLine("");
|
||||
|
||||
};
|
||||
_HtmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; };
|
||||
_HtmlSanitizer.AllowedAttributes.Add("class");
|
||||
_HtmlSanitizer.AllowedTags.Add("iframe");
|
||||
_HtmlSanitizer.AllowedTags.Remove("img");
|
||||
_HtmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
|
||||
_HtmlSanitizer.AllowedAttributes.Add("allowfullscreen");
|
||||
}
|
||||
|
||||
public string Sanitize(string raw)
|
||||
{
|
||||
return _HtmlSanitizer.Sanitize(raw);
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetOwnedStores(string userId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => (allowNoUser && string.IsNullOrEmpty(userId) ) || 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var query = ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString());
|
||||
|
||||
if (includeStore)
|
||||
{
|
||||
query = query.Include(data => data.StoreData);
|
||||
}
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return Array.Empty<ViewPointOfSaleViewModel.Item>();
|
||||
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 PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Description = Sanitize(c.GetDetailString("description")),
|
||||
Id = c.Key,
|
||||
Image = Sanitize(c.GetDetailString("image")),
|
||||
Title = Sanitize(c.GetDetailString("title") ?? c.Key),
|
||||
Price = c.GetDetail("price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
}).Single(),
|
||||
Custom = c.GetDetailString("custom") == "true"
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private class PosHolder
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public YamlMappingNode Value { get; set; }
|
||||
|
||||
public IEnumerable<PosScalar> GetDetail(string field)
|
||||
{
|
||||
var res = Value.Children
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(cc => cc.Key == field);
|
||||
return res;
|
||||
}
|
||||
|
||||
public string GetDetailString(string field)
|
||||
{
|
||||
|
||||
return GetDetail(field).FirstOrDefault()?.Value?.Value;
|
||||
}
|
||||
}
|
||||
private class PosScalar
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public YamlScalarNode Value { get; set; }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class CallbackController : Controller
|
||||
{
|
||||
public class CallbackSettings
|
||||
{
|
||||
public string Token
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
SettingsRepository _Settings;
|
||||
Network _Network;
|
||||
InvoiceWatcher _Watcher;
|
||||
ExplorerClient _Explorer;
|
||||
|
||||
public CallbackController(SettingsRepository repo,
|
||||
ExplorerClient explorer,
|
||||
InvoiceWatcher watcher,
|
||||
Network network)
|
||||
{
|
||||
_Settings = repo;
|
||||
_Network = network;
|
||||
_Watcher = watcher;
|
||||
_Explorer = explorer;
|
||||
}
|
||||
|
||||
[Route("callbacks/transactions")]
|
||||
[HttpPost]
|
||||
public async Task NewTransaction(string token)
|
||||
{
|
||||
await AssertToken(token);
|
||||
Logs.PayServer.LogInformation("New transaction callback");
|
||||
//We don't want to register all the json converter at MVC level, so we parse here
|
||||
var serializer = new NBXplorer.Serializer(_Network);
|
||||
var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync();
|
||||
var match = serializer.ToObject<TransactionMatch>(content);
|
||||
|
||||
foreach(var output in match.Outputs)
|
||||
{
|
||||
await _Watcher.NotifyReceived(output.ScriptPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("callbacks/blocks")]
|
||||
[HttpPost]
|
||||
public async Task NewBlock(string token)
|
||||
{
|
||||
await AssertToken(token);
|
||||
Logs.PayServer.LogInformation("New block callback");
|
||||
await _Watcher.NotifyBlock();
|
||||
}
|
||||
|
||||
private async Task AssertToken(string token)
|
||||
{
|
||||
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
|
||||
if(await GetToken() != token)
|
||||
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
|
||||
}
|
||||
|
||||
public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
|
||||
{
|
||||
string token = await GetToken();
|
||||
return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token);
|
||||
}
|
||||
|
||||
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
|
||||
{
|
||||
var uri = await GetCallbackUriAsync(request);
|
||||
await _Explorer.SubscribeToWalletAsync(uri, derivationScheme);
|
||||
}
|
||||
|
||||
private async Task<string> GetToken()
|
||||
{
|
||||
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
|
||||
if(callback == null)
|
||||
{
|
||||
callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() };
|
||||
await _Settings.UpdateSetting(callback);
|
||||
}
|
||||
var token = callback.Token;
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
|
||||
{
|
||||
string token = await GetToken();
|
||||
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
|
||||
}
|
||||
|
||||
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
|
||||
{
|
||||
var uri = await GetCallbackBlockUriAsync(request);
|
||||
await _Explorer.SubscribeToBlocksAsync(uri);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
119
BTCPayServer/Controllers/ChangellyController.cs
Normal file
119
BTCPayServer/Controllers/ChangellyController.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("[controller]/{storeId}")]
|
||||
public class ChangellyController : Controller
|
||||
{
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly RateFetcher _RateProviderFactory;
|
||||
|
||||
public ChangellyController(ChangellyClientProvider changellyClientProvider,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
RateFetcher rateProviderFactory)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("currencies")]
|
||||
public async Task<IActionResult> GetCurrencyList(string storeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
return Ok(await client.GetCurrenciesFull());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
|
||||
while (true)
|
||||
{
|
||||
if (callCounter > 10)
|
||||
{
|
||||
BadRequest();
|
||||
}
|
||||
|
||||
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
callCounter++;
|
||||
if (computedAmount < toCurrencyAmount)
|
||||
{
|
||||
currentAmount =
|
||||
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
|
||||
toCurrencyAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(currentAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Changelly> TryGetChangellyClient(string storeId)
|
||||
{
|
||||
var store = IsTest? null: HttpContext.GetStoreData();
|
||||
storeId = storeId ?? store?.Id;
|
||||
|
||||
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
}
|
||||
|
||||
public bool IsTest { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -5,16 +5,86 @@ 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;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
public HomeController(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
}
|
||||
public IActionResult Index()
|
||||
{
|
||||
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,121 +1,88 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
[BitpayAPIConstraint]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await CreateInvoiceCore(invoice, store);
|
||||
}
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
||||
if(invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
var resp = invoice.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
string token,
|
||||
DateTimeOffset? dateStart = null,
|
||||
DateTimeOffset? dateEnd = null,
|
||||
string orderId = null,
|
||||
string itemCode = null,
|
||||
string status = null,
|
||||
int? limit = null,
|
||||
int? offset = null)
|
||||
{
|
||||
if(dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
Skip = offset,
|
||||
EndDate = dateEnd,
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status,
|
||||
StoreId = store.Id
|
||||
};
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string 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(
|
||||
string token,
|
||||
DateTimeOffset? dateStart = null,
|
||||
DateTimeOffset? dateEnd = null,
|
||||
string orderId = null,
|
||||
string itemCode = null,
|
||||
string status = null,
|
||||
int? limit = null,
|
||||
int? offset = null)
|
||||
{
|
||||
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 == null ? null : new[] { orderId },
|
||||
ItemCode = itemCode == null ? null : new[] { itemCode },
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO()).ToArray();
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
|
||||
{
|
||||
if(facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if(expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if(token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if(token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.StoreId);
|
||||
if(store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
return store;
|
||||
}
|
||||
|
||||
}
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = _Network;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).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)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,277 +1,731 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
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.Invoices.Export;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
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
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[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();
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId, string command)
|
||||
{
|
||||
if(command == "refresh")
|
||||
{
|
||||
await _Watcher.WatchAsync(invoiceId, true);
|
||||
}
|
||||
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
|
||||
return RedirectToAction(nameof(Invoice), new
|
||||
{
|
||||
invoiceId = invoiceId
|
||||
});
|
||||
}
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
State = invoice.GetInvoiceState().ToString(),
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
"low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(dto.PosData)
|
||||
};
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId
|
||||
})).FirstOrDefault();
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
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 = _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 dto = invoice.EntityToDTO();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Rate = invoice.Rate,
|
||||
Fiat = dto.Price + " " + dto.Currency,
|
||||
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
|
||||
BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
|
||||
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
|
||||
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
BitcoinAddress = invoice.DepositAddress,
|
||||
PaymentUrl = dto.PaymentUrls.BIP72
|
||||
};
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
.Payments
|
||||
.Select(async payment =>
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
|
||||
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/{m.TransactionId}";
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
{
|
||||
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);
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
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);
|
||||
}
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BTCAddress = invoice.DepositAddress.ToString(),
|
||||
BTCAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
|
||||
BTCTotalDue = invoice.GetTotalCryptoDue().ToString(),
|
||||
BTCDue = invoice.GetCryptoDue().ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = invoice.Rate.ToString(),
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
|
||||
TxCount = invoice.GetTxCount(),
|
||||
BTCPaid = invoice.GetTotalPaid().ToString(),
|
||||
Status = invoice.Status
|
||||
};
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
}
|
||||
})
|
||||
.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);
|
||||
}
|
||||
|
||||
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return View(model);
|
||||
}
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var type = paymentMethodId.PaymentType.ToString();
|
||||
switch (paymentMethodId.PaymentType)
|
||||
{
|
||||
case PaymentTypes.BTCLike:
|
||||
type = "On-Chain";
|
||||
break;
|
||||
case PaymentTypes.LightningLike:
|
||||
type = "Off-Chain";
|
||||
break;
|
||||
}
|
||||
return $"{paymentMethodId.CryptoCode} ({type})";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if(expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString());
|
||||
if(expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00"));
|
||||
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
|
||||
return builder.ToString();
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
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;
|
||||
////
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
return Content(invoice.Status);
|
||||
}
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
}
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
foreach(var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = searchTerm,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId()
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
model.Skip = skip;
|
||||
model.Count = count;
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice()
|
||||
{
|
||||
var stores = await GetStores(GetUserId());
|
||||
if(stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
|
||||
}
|
||||
return View(new CreateInvoiceModel() { Stores = stores });
|
||||
}
|
||||
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
|
||||
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
model.Stores = await GetStores(GetUserId(), model.StoreId);
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
if(string.IsNullOrEmpty(store.DerivationStrategy))
|
||||
{
|
||||
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = "USD",
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store);
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
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 isDefaultPaymentId = false;
|
||||
if (paymentMethodId == null)
|
||||
{
|
||||
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
isDefaultPaymentId = true;
|
||||
}
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultPaymentId)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if (!isDefaultPaymentId)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
.FirstOrDefault();
|
||||
if (paymentMethodTemp == null)
|
||||
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
}
|
||||
|
||||
private async Task<SelectList> GetStores(string userId, string storeId = null)
|
||||
{
|
||||
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
}
|
||||
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
: null;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
(1m + (changelly.AmountMarkupPercentage / 100m)))
|
||||
: (decimal?)null;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
throw new NotSupportedException(),
|
||||
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
|
||||
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
#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,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
|
||||
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
CryptoImage = GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
return model;
|
||||
}
|
||||
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " (Lightning)";
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
|
||||
[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 == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[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(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();
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
if (message.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
|
||||
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
|
||||
{
|
||||
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
|
||||
return;
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
try
|
||||
{
|
||||
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
catch { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
[Route("invoice/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
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)
|
||||
{
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
};
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
|
||||
invoiceQuery.Count = count;
|
||||
invoiceQuery.Skip = skip;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = state.ToString(),
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete()
|
||||
});
|
||||
}
|
||||
model.Total = await counting;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.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,
|
||||
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
|
||||
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null)
|
||||
{
|
||||
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
invoiceQuery.Skip = 0;
|
||||
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)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice()
|
||||
{
|
||||
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
|
||||
if (stores.Count() == 0)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if (store == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
StatusMessage = null;
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationEmail = model.NotificationEmail,
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
catch (BitpayHttpException ex)
|
||||
{
|
||||
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
if (newState == "invalid")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice invalid",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?",
|
||||
});
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice complete",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
}
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ChangeInvoiceStateConfirm(string invoiceId, string newState)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId()
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,125 +1,307 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin.Payment;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
private InvoiceWatcher _Watcher;
|
||||
StoreRepository _StoreRepository;
|
||||
Network _Network;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProvider _FeeProvider;
|
||||
ExplorerClient _Explorer;
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ContentSecurityPolicies _CSP;
|
||||
RateFetcher _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly BTCPayWalletProvider _WalletProvider;
|
||||
IServiceProvider _ServiceProvider;
|
||||
public InvoiceController(
|
||||
IServiceProvider serviceProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RateFetcher rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ContentSecurityPolicies csp,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
_CSP = csp;
|
||||
}
|
||||
|
||||
public InvoiceController(
|
||||
Network network,
|
||||
InvoiceRepository invoiceRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
IRateProvider rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceWatcher watcher,
|
||||
ExplorerClient explorerClient,
|
||||
IFeeProvider feeProvider)
|
||||
{
|
||||
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
|
||||
_UserManager = userManager;
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
}
|
||||
|
||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||
{
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
|
||||
};
|
||||
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 + TimeSpan.FromMinutes(15.0);
|
||||
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = store.SpeedPolicy;
|
||||
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
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);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
{
|
||||
if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
|
||||
invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price);
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
|
||||
{
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
}
|
||||
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
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()
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
continue;
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
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());
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
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, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
{
|
||||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
string errorMessage = null;
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
compare = (a, b) => a > b;
|
||||
limitValue = storeBlob.LightningMaxValue;
|
||||
errorMessage = "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
|
||||
storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
compare = (a, b) => a < b;
|
||||
limitValue = storeBlob.OnChainMinValue;
|
||||
errorMessage = "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.NextNetworkFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if (transactionSpeed == null)
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||
policy = defaultPolicy;
|
||||
return policy;
|
||||
}
|
||||
|
||||
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
|
||||
{
|
||||
if (buyer == null)
|
||||
return;
|
||||
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
|
||||
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
|
||||
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
|
||||
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
|
||||
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
|
||||
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
|
||||
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
BTCPayServer/Controllers/Macaroons.cs
Normal file
57
BTCPayServer/Controllers/Macaroons.cs
Normal file
@ -0,0 +1,57 @@
|
||||
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))
|
||||
return macaroons;
|
||||
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 Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
public Macaroon AdminMacaroon { get; set; }
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,40 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
62
BTCPayServer/Controllers/PublicController.cs
Normal file
62
BTCPayServer/Controllers/PublicController.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PublicController : Controller
|
||||
{
|
||||
public PublicController(InvoiceController invoiceController,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
private InvoiceController _InvoiceController;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
ModelState.AddModelError("Store", "Invalid store");
|
||||
else
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
||||
}
|
||||
|
||||
if (model == null || model.Price <= 0)
|
||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
NotificationEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
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]
|
||||
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(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; }
|
||||
}
|
||||
}
|
@ -7,34 +7,232 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProvider _RateProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class RateController : Controller
|
||||
{
|
||||
RateFetcher _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
|
||||
{
|
||||
var allRates = (await _RateProvider.GetRatesAsync());
|
||||
return new DataWrapper<NBitpayClient.Rate[]>
|
||||
(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
public TokenRepository TokenRepository { get; }
|
||||
|
||||
}
|
||||
}
|
||||
public RateController(
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
TokenRepository = tokenRepository;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var err = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
err.StatusCode = 404;
|
||||
return err;
|
||||
}
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
|
||||
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
|
||||
.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
|
||||
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
|
||||
|
||||
var result = await GetRates2(currencypairs, store.Id);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
|
||||
[Route("rates/{baseCurrency}/{currency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<Rate>(rates.First()));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
private async Task<string> GetStoreId(string storeId)
|
||||
{
|
||||
if (storeId != null && this.HttpContext.GetStoreData()?.Id == storeId)
|
||||
return storeId;
|
||||
if(storeId == null)
|
||||
{
|
||||
var tokens = await this.TokenRepository.GetTokens(this.User.GetSIN());
|
||||
storeId = tokens.Select(s => s.StoreId).Where(s => s != null).FirstOrDefault();
|
||||
}
|
||||
if (storeId == null)
|
||||
return null;
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return null;
|
||||
this.HttpContext.SetStoreData(store);
|
||||
return storeId;
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
|
||||
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
|
||||
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||
foreach (var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if (!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
{
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
CurrencyPair = r.Pair.ToString(),
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
|
||||
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
|
||||
{
|
||||
StringBuilder currencyPairsBuilder = new StringBuilder();
|
||||
bool first = true;
|
||||
foreach (var currencyCode in currencyCodes)
|
||||
{
|
||||
if(!first)
|
||||
currencyPairsBuilder.Append(",");
|
||||
first = false;
|
||||
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
|
||||
}
|
||||
return currencyPairsBuilder.ToString();
|
||||
}
|
||||
|
||||
public class Rate
|
||||
{
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "cryptoCode")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currencyPair")]
|
||||
public string CurrencyPair
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,97 +1,821 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Validations;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
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;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Roles = Roles.ServerAdmin)]
|
||||
public class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
}
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
public IActionResult ListUsers()
|
||||
{
|
||||
var users = new UsersViewModel();
|
||||
users.Users
|
||||
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
|
||||
{
|
||||
Name = u.UserName,
|
||||
Email = u.Email
|
||||
}).ToList();
|
||||
return View(users);
|
||||
}
|
||||
[Route("server/rates")]
|
||||
public async Task<IActionResult> Rates()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
var vm = new RatesViewModel()
|
||||
{
|
||||
CacheMinutes = rates.CacheInMinutes,
|
||||
PrivateKey = rates.PrivateKey,
|
||||
PublicKey = rates.PublicKey
|
||||
};
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies upadated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
private static async Task FetchRateLimits(RatesViewModel vm)
|
||||
{
|
||||
var coinAverage = GetCoinaverageService(vm, false);
|
||||
if (coinAverage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
if(command == "Test")
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
return View(model);
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
ModelState.Remove(nameof(model.TestEmail));
|
||||
if(!ModelState.IsValid)
|
||||
return View(model);
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
[Route("server/rates")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Rates(RatesViewModel vm)
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
rates.PrivateKey = vm.PrivateKey;
|
||||
rates.PublicKey = vm.PublicKey;
|
||||
rates.CacheInMinutes = vm.CacheMinutes;
|
||||
try
|
||||
{
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if (service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
await _SettingsRepository.UpdateSetting(rates);
|
||||
StatusMessage = "Rate settings successfully updated";
|
||||
return RedirectToAction(nameof(Rates));
|
||||
}
|
||||
|
||||
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
|
||||
{
|
||||
var settings = new CoinAverageSettings()
|
||||
{
|
||||
KeyPair = (vm.PublicKey, vm.PrivateKey)
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
public IActionResult ListUsers()
|
||||
{
|
||||
var users = new UsersViewModel();
|
||||
users.StatusMessage = StatusMessage;
|
||||
users.Users
|
||||
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
|
||||
{
|
||||
Name = u.UserName,
|
||||
Email = u.Email,
|
||||
Id = u.Id
|
||||
}).ToList();
|
||||
return View(users);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.Email = user.Email;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
public IActionResult Maintenance()
|
||||
{
|
||||
MaintenanceViewModel vm = new MaintenanceViewModel();
|
||||
vm.UserName = "btcpayserver";
|
||||
vm.DNSDomain = this.Request.Host.Host;
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
vm.DNSDomain = null;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (command == "changedomain")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
|
||||
return View(vm);
|
||||
}
|
||||
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
|
||||
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
|
||||
return View(vm);
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
|
||||
return View(vm);
|
||||
}
|
||||
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
|
||||
return View(vm);
|
||||
}
|
||||
var builder = new UriBuilder();
|
||||
using (var client = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
}))
|
||||
{
|
||||
try
|
||||
{
|
||||
builder.Scheme = this.Request.Scheme;
|
||||
builder.Host = 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();
|
||||
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
|
||||
if (!hasCommonAddress)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
messages.Add(ex.Message);
|
||||
if (ex.InnerException != null)
|
||||
messages.Add(ex.InnerException.Message);
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var error = RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
|
||||
if (error != null)
|
||||
return error;
|
||||
|
||||
builder.Path = null;
|
||||
builder.Query = null;
|
||||
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-update.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
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")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult SeeRunId(string expected = null)
|
||||
{
|
||||
if (expected == RunId)
|
||||
return Ok();
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
|
||||
{
|
||||
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
|
||||
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
|
||||
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
|
||||
|
||||
if (_Options.TrustedFingerprints.Count != 0)
|
||||
{
|
||||
sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
|
||||
{
|
||||
if (_Options.TrustedFingerprints.Count == 0)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
|
||||
}
|
||||
else
|
||||
{
|
||||
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
|
||||
if (!e.CanTrust)
|
||||
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
sshClient.Connect();
|
||||
}
|
||||
catch (Renci.SshNet.Common.SshAuthenticationException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
|
||||
{
|
||||
message = aggrEx.InnerException.Message;
|
||||
}
|
||||
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var sshCommand = sshClient.CreateCommand(ssh);
|
||||
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
|
||||
sshCommand.BeginExecute(ar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation("Running SSH command: " + ssh);
|
||||
var result = sshCommand.EndExecute(ar);
|
||||
Logs.PayServer.LogInformation("SSH command executed: " + result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
|
||||
}
|
||||
sshClient.Dispose();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete user " + user.Email,
|
||||
Description = "This user will be permanently deleted",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeleteUserPost(string userId)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
await _StoreRepository.CleanUnreachableStores();
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
Action = nameof(LndServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
i = 0;
|
||||
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Spark server",
|
||||
Action = nameof(SparkServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Lightning charge server",
|
||||
Action = nameof(LightningChargeServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
foreach(var externalService in _Options.ExternalServices)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
{
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
|
||||
});
|
||||
}
|
||||
if(_Options.SSHSettings != null)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (lightningCharge == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
|
||||
vm.APIToken = lightningCharge.Password;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
|
||||
{
|
||||
if (lightningCharge.CookieFilePath != "fake")
|
||||
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
|
||||
else
|
||||
vm.APIToken = "fake";
|
||||
}
|
||||
var builder = new UriBuilder(lightningCharge.ToUri(false));
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/services/spark/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if(spark == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
SparkServicesViewModel vm = new SparkServicesViewModel();
|
||||
vm.ShowQR = showQR;
|
||||
try
|
||||
{
|
||||
var cookie = (spark.CookeFile == "fake"
|
||||
? "fake:fake:fake" // If we are testing, it should not crash
|
||||
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
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)
|
||||
return NotFound();
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if(external.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
model.Uri = external.BaseUri.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (external.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
}
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetLNDConfig(uint configKey)
|
||||
{
|
||||
var conf = _LnConfigProvider.GetConfig(configKey);
|
||||
if (conf == null)
|
||||
return NotFound();
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = external.BaseUri.DnsSafeHost;
|
||||
grpcConf.Port = external.BaseUri.Port;
|
||||
grpcConf.SSL = external.BaseUri.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = external.BaseUri.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(external.ConnectionType.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndServices), 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/ssh")]
|
||||
public IActionResult SSHService(bool downloadKeyFile = false)
|
||||
{
|
||||
var settings = _Options.SSHSettings;
|
||||
if (settings == null)
|
||||
return NotFound();
|
||||
if (downloadKeyFile)
|
||||
{
|
||||
if (!System.IO.File.Exists(settings.KeyFile))
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/theme")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
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")
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
238
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
238
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[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 HardwareWalletService(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 = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
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, MvcJsonOptions.Value.SerializerSettings));
|
||||
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();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
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.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
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);
|
||||
|
||||
showAddress = showAddress && strategy != null;
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
}
|
||||
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\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
96
BTCPayServer/Controllers/StoresController.Changelly.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/changelly")]
|
||||
public IActionResult UpdateChangellySettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().ChangellySettings;
|
||||
if (existing == null) return;
|
||||
vm.ApiKey = existing.ApiKey;
|
||||
vm.ApiSecret = existing.ApiSecret;
|
||||
vm.ApiUrl = existing.ApiUrl;
|
||||
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
vm.ShowFiat = existing.ShowFiat;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/changelly")]
|
||||
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var changellySettings = new ChangellySettings()
|
||||
{
|
||||
ApiKey = vm.ApiKey,
|
||||
ApiSecret = vm.ApiSecret,
|
||||
ApiUrl = vm.ApiUrl,
|
||||
ChangellyMerchantId = vm.ChangellyMerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage,
|
||||
ShowFiat = vm.ShowFiat
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.ChangellySettings = changellySettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Changelly settings modified";
|
||||
_changellyClientProvider.InvalidateClient(storeId);
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
72
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
@ -0,0 +1,72 @@
|
||||
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;
|
||||
}
|
||||
|
||||
[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
|
||||
};
|
||||
|
||||
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});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
184
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
184
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
|
||||
}
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
|
||||
{
|
||||
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
|
||||
{
|
||||
return CanUseInternalLightning() ? connectionString : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if(connectionString.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if(connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if(!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
case "test" when paymentMethod == null:
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
case "test":
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning()
|
||||
{
|
||||
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
121
BTCPayServer/Controllers/UserStoresController.cs
Normal file
121
BTCPayServer/Controllers/UserStoresController.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
public UserStoresController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_Repo = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public IActionResult CreateStore()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public string CreatedStoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
StatusMessage = "Store removed successfully";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
664
BTCPayServer/Controllers/WalletsController.cs
Normal file
664
BTCPayServer/Controllers/WalletsController.cs
Normal file
@ -0,0 +1,664 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("wallets")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class WalletsController : Controller
|
||||
{
|
||||
public StoreRepository Repository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public ExplorerClientProvider ExplorerClientProvider { get; }
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||
private readonly NBXplorerDashboard _dashboard;
|
||||
|
||||
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,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
NBXplorerDashboard dashboard,
|
||||
RateFetcher rateProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
RateFetcher = rateProvider;
|
||||
NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
_mvcJsonOptions = mvcJsonOptions;
|
||||
_dashboard = dashboard;
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
var stores = await Repository.GetStoresByUserId(GetUserId());
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
Network: d.Network)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(_ => (Wallet: _.Wallet,
|
||||
Store: s,
|
||||
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
|
||||
DerivationStrategy: _.DerivationStrategy,
|
||||
Network: _.Network)))
|
||||
.ToList();
|
||||
|
||||
foreach (var wallet in onChainWallets)
|
||||
{
|
||||
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
|
||||
wallets.Wallets.Add(walletVm);
|
||||
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
|
||||
if (!wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
{
|
||||
walletVm.Balance = "";
|
||||
}
|
||||
walletVm.CryptoCode = wallet.Network.CryptoCode;
|
||||
walletVm.StoreId = wallet.Store.Id;
|
||||
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
|
||||
walletVm.StoreName = wallet.Store.StoreName;
|
||||
walletVm.IsOwner = wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key);
|
||||
}
|
||||
|
||||
return View(wallets);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}")]
|
||||
public async Task<IActionResult> WalletTransactions(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
{
|
||||
var vm = new ListTransactionsViewModel.TransactionViewModel();
|
||||
model.Transactions.Add(vm);
|
||||
vm.Id = tx.TransactionId.ToString();
|
||||
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(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");
|
||||
WalletSendModel model = new WalletSendModel()
|
||||
{
|
||||
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.DerivationStrategyBase);
|
||||
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
|
||||
model.Fiat = currencyPair.Right;
|
||||
}
|
||||
else
|
||||
{
|
||||
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
model.AdvancedMode = advancedMode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm, string command = null)
|
||||
{
|
||||
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();
|
||||
|
||||
if (command == "noob" || command == "expert")
|
||||
{
|
||||
ModelState.Clear();
|
||||
vm.AdvancedMode = command == "expert";
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
|
||||
{
|
||||
Destination = vm.Destination,
|
||||
Amount = vm.Amount.Value,
|
||||
SubstractFees = vm.SubstractFees,
|
||||
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
|
||||
NoChange = vm.NoChange
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger")]
|
||||
public async Task<IActionResult> WalletSendLedger(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendLedgerModel vm)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination = destination?.Trim();
|
||||
return new IDestination[] { BitcoinAddress.Create(destination, network) };
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new RescanWalletModel();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
|
||||
// We need to ensure it is segwit,
|
||||
// because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
|
||||
vm.IsSegwit = paymentMethod.DerivationStrategyBase.IsSegwit();
|
||||
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)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
|
||||
{
|
||||
if (scanProgress.Progress == null)
|
||||
{
|
||||
vm.Progress = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Progress = scanProgress.Progress.OverallProgress;
|
||||
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
|
||||
}
|
||||
}
|
||||
if (scanProgress.Status == ScanUTXOStatus.Complete)
|
||||
{
|
||||
vm.LastSuccess = scanProgress.Progress;
|
||||
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
|
||||
}
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/rescan")]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, RescanWalletModel vm)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
|
||||
}
|
||||
return RedirectToAction();
|
||||
}
|
||||
|
||||
private string GetCurrencyCode(string defaultLang)
|
||||
{
|
||||
if (defaultLang == null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var ri = new RegionInfo(defaultLang);
|
||||
return ri.ISOCurrencySymbol;
|
||||
}
|
||||
catch (ArgumentException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
|
||||
{
|
||||
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/success")]
|
||||
public IActionResult WalletSendLedgerSuccess(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
string txid)
|
||||
{
|
||||
StatusMessage = $"Transaction broadcasted ({txid})";
|
||||
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/ws")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
string command,
|
||||
// getinfo
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
bool noChange = false,
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
|
||||
var cryptoCode = walletId.CryptoCode;
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||
|
||||
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);
|
||||
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.Trim(), 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 == "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 keypaths = new Dictionary<Script, KeyPath>();
|
||||
List<Coin> availableCoins = new List<Coin>();
|
||||
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
availableCoins.Add(c.Coin);
|
||||
}
|
||||
|
||||
var changeAddress = await change;
|
||||
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||
// Some deployment have the wallet root key path saved in the store blob
|
||||
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
|
||||
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
|
||||
{
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
|
||||
storeData.SetStoreBlob(storeBlob);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
retry:
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.AddCoins(availableCoins);
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
|
||||
if (noChange && hasChange)
|
||||
{
|
||||
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
|
||||
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
|
||||
subsctractFeesValue = true;
|
||||
goto retry;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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() };
|
||||
}
|
||||
}
|
||||
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, _mvcJsonOptions.Value.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
}
|
||||
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 class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user