Compare commits
839 Commits
Author | SHA1 | Date | |
---|---|---|---|
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 | |||
1ef59e05a5 | |||
5e09992637 | |||
30448233b1 | |||
a1601a17aa | |||
7522f7d0f7 | |||
d04b9c4c09 | |||
1a24ff9a49 | |||
13d72de82d | |||
3728fdab3f | |||
2317e3d50c | |||
5f15976c02 | |||
7f592639c5 | |||
a98402af12 | |||
316ffa91d1 | |||
c24953b57e | |||
7a1b1b7e5e | |||
70f71f64c4 | |||
5bccd07d7d | |||
d818baa6d1 | |||
249b8abf03 | |||
c134277514 | |||
f5d366cf7f | |||
ad25a2ed08 | |||
1e7a2ffe97 | |||
dd52075ff1 | |||
0253e42bd5 | |||
d99774f8d9 | |||
d563a2ec89 | |||
b4b4523193 | |||
fbcb69f447 | |||
8ae5a9c1f7 | |||
3ef5bfb6eb | |||
4016ded584 | |||
5b0b4adb1c | |||
f1ec3b0c75 | |||
b5d55a2066 | |||
0d2c9fe377 | |||
2c7cc9a796 | |||
2e1d623755 | |||
52fee8f842 | |||
6ba17e8e30 | |||
ac3432920a | |||
63c88be533 | |||
3cb577e6ba | |||
1e0d64c548 | |||
bc1b9ff59c | |||
7d73bed3be | |||
126fbdfd60 | |||
15094436fd | |||
010c653995 | |||
119f82fd4e | |||
3bbf4de5d2 | |||
0807f3b87b | |||
4e9b3b40aa | |||
cc444811db | |||
50c8525012 | |||
aedad497e8 | |||
b1b231e645 | |||
dc46fd225a | |||
6226de7cff | |||
37327ec674 | |||
c071c81403 | |||
85d75a013a | |||
3816b36131 | |||
dc7965267b | |||
ce9a6bced7 | |||
85325dc710 | |||
ac4050df70 | |||
a16a53167b | |||
afab3cf847 | |||
8fdaeb7bac | |||
7e0f9f6e0d | |||
5b1bf6cd88 | |||
b1584c352b | |||
b06b83503c | |||
b03d89c190 | |||
f53548d10f | |||
5ec2f54d7f | |||
db588ff961 | |||
2b7006a14c | |||
8f5f07882f | |||
0eee8e7464 | |||
3725a5b644 | |||
c84c0ac64d | |||
098e07988c | |||
66bb702aca | |||
03ff2fedf0 | |||
c707f47b11 | |||
585efa3ff5 | |||
07d0b98a23 | |||
c7c0f01010 | |||
cf6b17250a | |||
90503a490c | |||
ebdd53b99b | |||
51a5d2e812 | |||
2ad509d56a | |||
1a98bfba36 | |||
d05bb6c60e | |||
ed81b6a6aa | |||
264914588f | |||
05df43b426 | |||
0334a4e176 | |||
38dca425da | |||
82d4a79dd4 | |||
6725be8145 | |||
f5b693f01b | |||
f09f23e570 | |||
4f4d05b8cd | |||
0c5b5ff49c | |||
a815fad3f1 | |||
d8b1c7c10a | |||
02e1aea80c | |||
1892f7e0f4 | |||
b7b50349a7 | |||
02d227ee02 | |||
47f8938b89 | |||
4945a640a7 | |||
0136977359 | |||
0acd3e20b0 | |||
30bdfeee37 | |||
7ea665d884 | |||
073edcfb12 | |||
a645366a25 | |||
12aa0b7abd | |||
3f98a50410 | |||
24c8c076d5 | |||
37e6931d33 | |||
86493568e9 | |||
bb51436ae3 | |||
854a55ac1a | |||
cfb4b080d3 | |||
00aa2e4e17 | |||
69c67d99f6 | |||
65596ec8c1 | |||
49643cb00e | |||
35b0faee57 | |||
88ef4d69b2 | |||
575b6ca222 | |||
b5a0e844d2 | |||
2642e11ce2 | |||
b4fe655efe | |||
ffb761909a | |||
b443e1ac6e | |||
a4792f54a7 | |||
686ae029e0 | |||
f3fd2e7d0f | |||
7efd9ba0a5 | |||
1c2a6bb8a1 | |||
7bcf1cbdd5 | |||
2aaa2544bd | |||
d85f03ba20 | |||
cfb51a6be4 | |||
c9d778c94b | |||
fd62f882de | |||
adc050f190 | |||
2d551b9fc5 | |||
884acdde32 | |||
8f896de794 | |||
5e4e26d2fd | |||
ae688e6615 | |||
c4c812bdf6 | |||
e620fc0283 | |||
c333902468 | |||
4c83ecd06a | |||
b28a547dc4 | |||
6bc17e05bd | |||
0903350d30 | |||
6c0f19b457 | |||
e119dc823f | |||
43295c9c57 | |||
ded8b54042 | |||
50a3178d51 | |||
393c226032 | |||
f2630df387 | |||
abcd2c1750 | |||
cc95f3b5b5 | |||
a08ee93b43 | |||
4b90f873d5 | |||
419ab8e0b1 | |||
c95ef27998 | |||
63dfd93834 | |||
57610881de | |||
7469faf296 | |||
55a884a559 | |||
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 | |||
964e541c32 | |||
78fec4ed22 | |||
ef111d36c9 | |||
4f64193e85 | |||
89bb6d1268 | |||
9f4226bf0f | |||
a87c2a3374 | |||
d7294ba5a0 | |||
82d286dc6f | |||
1fa18ab997 | |||
afc90f32c9 | |||
e9cfb7c21e | |||
1af8ea3769 | |||
9f7af190f1 | |||
9c703fe94d | |||
a7a11a4f13 | |||
c32c3bb62b | |||
e29d1480a6 | |||
8f299d7791 | |||
65fb2e992e | |||
41f5d677d5 | |||
a2b78b8cd9 | |||
c93f217033 | |||
82c47b6e9a | |||
94fb738c67 | |||
89071e40fc | |||
95a90c410e | |||
59fc371cd5 | |||
9b404e330d | |||
1667f9b2ef | |||
caadfc8641 | |||
bffc2e70c1 | |||
8b686f0b12 | |||
def8d1e0cb | |||
ca28c34be0 | |||
196bc3ea00 | |||
b15267be4d | |||
5c074f6f5f | |||
04cba61888 | |||
a41e2e1ceb | |||
d1d03c98ba | |||
679942159e | |||
3e48a54ab5 | |||
f6e389ff62 | |||
63c309bd12 | |||
561ec57cc8 | |||
3cefd7bd1e | |||
c63feb488c | |||
12c418d84d | |||
4b982f815c | |||
d4d3346b6d | |||
6010a103e0 | |||
5dc1da2af0 | |||
f2ccc4d963 | |||
a92d48efdd | |||
b633206b45 | |||
b6f3d2af5e | |||
5fd77d9fcc | |||
5ca4494eed | |||
de7e419ef4 | |||
20a6b3fc33 | |||
540414d8f5 | |||
abcdb8ced0 | |||
5076d73695 | |||
d88735f84e | |||
2244f0ab76 | |||
40c85d6104 | |||
42892e24f4 | |||
e6357d2ac8 | |||
1eecd85ceb | |||
c27557826b | |||
88150b6535 | |||
d63176da19 | |||
887da5aa9a | |||
6e7f1151bc | |||
b2aebcc5d3 | |||
ae9ad0fa65 | |||
fb6d852827 | |||
ba17612461 | |||
a15c7a0213 | |||
7e321d4016 | |||
a05cd5678b | |||
2ccf007b9a | |||
895b8c2c80 | |||
0f175174f6 | |||
493466683c | |||
761c342c51 | |||
5341da28d9 | |||
7768f41849 | |||
fa8993191e | |||
239ce28575 | |||
c52a49f747 | |||
e4b9895ba7 | |||
92a2bb4d32 | |||
bfec722312 | |||
5a3f7b5b70 | |||
2aa097be46 | |||
8a646d85c6 | |||
890b3eaa00 | |||
cda28ebf15 | |||
2245027ca3 | |||
3dc250f801 | |||
66e786a1b0 | |||
1e26926350 | |||
8bd7ea5bbc | |||
6eb36abe2e | |||
6fced3fab2 | |||
774e456e54 | |||
519859e1c5 | |||
26f0c488e5 | |||
1b0b53fbd0 | |||
35f4ea29f9 | |||
fff39d9879 | |||
444e761d41 | |||
68a3def35a | |||
d9426d301d | |||
8bcf7109a3 | |||
3effdf0f4d | |||
3c122bcf53 | |||
037ff52f4f | |||
b11f8acba1 | |||
ef9a633aa4 | |||
c7e2f979dd | |||
e97bb9c933 | |||
fa506b5bf8 | |||
e3193a92d0 | |||
d76dabdca6 | |||
d219f50912 | |||
e08710a19c | |||
4e167b35be | |||
16873384a8 | |||
f87339f9fa | |||
5f5e5e3211 | |||
f724db8226 | |||
c7e90cd7df | |||
8c5b00b1a3 | |||
a3b79fbcd8 | |||
9db77e6351 | |||
5bc1eaec9f | |||
81c9ce7284 | |||
caa6978d80 | |||
af22d6a4e3 | |||
0eabb3c37c | |||
2b84791391 | |||
6f896cb096 | |||
9a488c60f2 | |||
9cb50446f4 | |||
8c00a2359e | |||
d1ff34d16d | |||
8e8615dab8 | |||
a63ed4d3b4 | |||
968c820702 | |||
3061b4dfd2 | |||
ed4de612dd | |||
d4bdd5fd9c | |||
8b71556425 | |||
ae6e1bfd85 | |||
6d1f3b73ef | |||
0dcaf80c7f | |||
fc9cd5bdf0 | |||
a434c45196 | |||
87b316ec23 | |||
9c99ffae57 | |||
30a3a84ec9 | |||
d0f585df9d | |||
bac2db5cda | |||
c35bf2f483 | |||
2e04c5e39c | |||
9dcf16e819 | |||
361d494cde | |||
4d7015294e | |||
5f16fb4668 | |||
4bf2228675 | |||
2ba823f192 | |||
27fa2d5b69 | |||
47ef7661d8 | |||
3f6ff25322 | |||
f56c23009a | |||
e80593fb7b | |||
57324345ac | |||
73e280157d | |||
cfaa5766ed | |||
8b08db308b | |||
3e2ff55954 | |||
70d1d0d230 | |||
94e0048a3b | |||
a34d1641b3 | |||
40c645e433 | |||
b2e5415a35 | |||
365ee4cf0b | |||
2b4603a234 | |||
ec23eae21d | |||
7a9229628a | |||
9db5c0f375 | |||
27bde55f54 | |||
2bb24282d2 | |||
998472e463 | |||
63ff46a768 | |||
660f43e3b7 | |||
0ba96aa4b8 | |||
d85247d2ad | |||
9ca85ed365 | |||
93113fd871 | |||
b5d360594a | |||
d5ae79c38c | |||
7cf07b27e3 | |||
bb0f986b0c | |||
2c2a85327f | |||
7bf03e497b | |||
7a4dee3d38 | |||
7b27d6f0bb | |||
83dc95a0a7 | |||
d60889f952 | |||
8c9952973d | |||
00673bdb7f | |||
d039890a9b | |||
41e88c07fe | |||
67c5027b16 | |||
a341d4f800 | |||
e3833914b3 | |||
49cdec6961 | |||
3ad1834439 | |||
4b492eae85 | |||
f0ff47af8d | |||
991826b686 | |||
22d59a1ed7 | |||
475ea68696 | |||
864e84706a | |||
9c93e76eeb | |||
7fa1b65af0 | |||
c00c95efcf | |||
94be2b46d5 | |||
4b4d0d2d19 | |||
0d06cf63b7 | |||
7b24c02d51 | |||
becf488714 | |||
e89e8226e4 | |||
a533a96598 | |||
27321c0919 | |||
058472d325 | |||
b5c9a03052 | |||
07dad3affa | |||
8afc103ae7 | |||
591d7b4b80 | |||
2162afc78e | |||
25e226d219 | |||
8472bfe90d | |||
93645b2fbe | |||
d53c987f2e | |||
682693a9f0 | |||
e836faf792 | |||
6e27233be8 | |||
9209984a2f | |||
1477630c78 | |||
ab670080c7 | |||
8198f98376 | |||
65b4697229 | |||
e75a1a8b70 | |||
580494fea7 | |||
5a958da84d | |||
cad602ad14 | |||
1f14bd6188 | |||
156f52b76f | |||
d674b8ac71 | |||
861150971f | |||
a653421514 | |||
8f234a02cb | |||
92ecf99427 | |||
705dbf12d7 | |||
fe11b11c13 | |||
f2a43ad1f3 | |||
cbbe5cfb25 | |||
0eccc6085b | |||
a89da1f705 | |||
5b297e539a | |||
1d932c3753 | |||
5a77fc74ba | |||
7b47b96252 | |||
a4bec83ecc | |||
8509a0de18 | |||
8e30b7430d | |||
9235d32a45 | |||
dd503570ac | |||
613281a1e7 | |||
bab7bf6633 | |||
1831692761 | |||
e144d2479b | |||
c25831316e | |||
60b72aabe8 | |||
c8fcb0ab18 | |||
9911d18390 | |||
e24630ac1e | |||
4c1fd3edae | |||
f65492dd66 | |||
5d978c7670 | |||
11788cece9 | |||
1aaa55dc62 | |||
ce57a2b8fb | |||
0604cc5bd0 | |||
3d2c0bcc6c | |||
0f222979a6 | |||
a1eb6a14f5 | |||
186ce01022 | |||
0096ec1d12 | |||
2929d7bf51 | |||
d90fb5764d | |||
4dccd0c733 | |||
300d912331 | |||
9d21c89151 | |||
24a8c4015c | |||
5eb40d6b7f | |||
36f486e91b | |||
5b684ac26e | |||
85062725bd | |||
401d9c8565 | |||
6f276ac1bc | |||
4350785cef | |||
9a2a85ac3d | |||
d030a61322 | |||
dacb6dca41 | |||
c40fc69087 | |||
eff983135c | |||
479303dd9e | |||
e9b2088f7d | |||
4af5b94013 | |||
441398402d | |||
258d4fda3f | |||
8e667f6c3f | |||
a996cc2e6d | |||
9b8a8690e7 | |||
f2387fd6b5 | |||
888036a99d | |||
539c0ed7f0 | |||
95e065a462 | |||
087f20cb6c | |||
7adf321956 | |||
dc749462ec | |||
16b57f24a2 | |||
b16b1c3e8b | |||
fee56873b5 | |||
e1b2b72cd2 | |||
daf4e5ce6c | |||
2ec2c7263f | |||
abfcab552f | |||
cfdf8b1670 | |||
f23e2a3ec4 | |||
aa1ac3da50 | |||
c9c7316b7d | |||
d152d5cd90 | |||
6fd37710e1 | |||
0419a3c19a | |||
0c382da561 | |||
9fc7f287d2 | |||
dd7c4850f0 | |||
93992ec3ed | |||
15d9adfbf1 | |||
676a914c40 | |||
b423b4eec1 | |||
9784a89112 | |||
7b596e6d9c | |||
76febcf238 | |||
a57a72de88 | |||
235b307b06 | |||
05b0f6d0f7 | |||
1d7081d8b8 | |||
c0174c0c2c | |||
fa8324c1f9 | |||
4b0951caec | |||
0d51c99717 | |||
24623c59d7 | |||
88044f6b76 | |||
38edbf8362 | |||
bc0acf5701 | |||
a82f181126 | |||
be0139a46f | |||
4db5b4f2b1 | |||
93cefced80 | |||
85f586f623 | |||
2be1f97419 | |||
63014231ab | |||
3ac37497ab | |||
d0cafb020f | |||
d3b3198b68 | |||
c1f17ff63b | |||
dafd958f69 | |||
f51af6c61c | |||
254db22063 | |||
8be4256278 | |||
8e8669d63f | |||
4625ff92f1 | |||
6aa84326af | |||
9a384d81fe | |||
0cbe36c048 | |||
7f16aa8c7e | |||
872f8a6229 | |||
9b261daa6d | |||
c46c15c258 | |||
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 | |||
8cafa8a483 | |||
724af44e41 | |||
bac9ef4f93 | |||
ada6f3b844 | |||
c8a26ce952 | |||
6cf80b7533 | |||
79df523bb2 | |||
e921f9757a | |||
bed9737d64 | |||
2583eb15ec | |||
1879ea55e8 |
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]+)*/
|
@ -6,16 +6,16 @@
|
||||
<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.7.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</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>
|
||||
@ -25,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,4 +1,6 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Linq;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -32,9 +34,16 @@ using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public enum TestDatabases
|
||||
{
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
@ -57,6 +66,11 @@ namespace BTCPayServer.Tests
|
||||
set;
|
||||
}
|
||||
|
||||
public string MySQL
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
@ -68,6 +82,10 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
public TestDatabases TestDatabase
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool MockRates { get; set; } = true;
|
||||
|
||||
@ -94,7 +112,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (Postgres != null)
|
||||
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());
|
||||
@ -102,7 +122,7 @@ namespace BTCPayServer.Tests
|
||||
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 });
|
||||
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 =>
|
||||
@ -120,30 +140,72 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
|
||||
rateProvider.DirectProviders.Clear();
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
if (MockRates)
|
||||
{
|
||||
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_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
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
|
||||
@ -161,17 +223,21 @@ namespace BTCPayServer.Tests
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
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");
|
||||
context.Request.Host = new HostString("127.0.0.1", Port);
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
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)
|
||||
if (storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
|
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
|
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CoinSwitchTests
|
||||
{
|
||||
public CoinSwitchTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.Null(storeBlob.CoinSwitchSettings);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
|
||||
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
|
||||
updateModel.MerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
304
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
304
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.UnitTest1;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CrowdfundTests
|
||||
{
|
||||
public CrowdfundTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateAndDeleteCrowdfundApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
var apps = user.GetController<AppsController>();
|
||||
var apps2 = user2.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanContributeOnlyWhenAllowed()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
//Scenario 1: Not Enabled - Not Allowed
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
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");
|
||||
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
Logs.Tester.LogInformation("We create an invoice with a hardcap");
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
|
||||
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
|
||||
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
|
||||
Assert.Equal(0m, model.Info.CurrentAmount );
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount);
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
|
||||
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(1m, model.Info.CurrentAmount);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
|
||||
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
|
||||
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
|
||||
|
||||
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
|
||||
crowdfundViewModel.EnforceTargetAmount = false;
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Threading.Channels;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
TaskCompletionSource<bool> _Evt = null;
|
||||
IWebHost _Host = null;
|
||||
CancellationTokenSource _Closed = new CancellationTokenSource();
|
||||
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
|
||||
public CustomServer()
|
||||
{
|
||||
{
|
||||
var port = Utils.FreeTcpPort();
|
||||
_Host = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(req =>
|
||||
{
|
||||
while (_Act == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
_Closed.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
_Act(req);
|
||||
_Act = null;
|
||||
_Evt.TrySetResult(true);
|
||||
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
public async Task<JObject> GetNextRequest()
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>();
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>();
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
|
||||
{
|
||||
_Evt.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
try
|
||||
{
|
||||
JObject req = null;
|
||||
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
|
||||
!_Requests.Reader.TryRead(out req))
|
||||
{
|
||||
|
||||
}
|
||||
return req;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
FROM 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"]
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningRPCClient RPC { get; }
|
||||
public CLightningClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
Client = new LndClient(Swagger, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public LndClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
||||
|
@ -1,246 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using System.Globalization;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
// this depends for now on `docker-compose up devlnd`
|
||||
public class UnitTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public UnitTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
initializeEnvironment();
|
||||
|
||||
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
||||
|
||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
||||
}
|
||||
|
||||
private LndSwaggerClient MerchantLnd { get; set; }
|
||||
private LndInvoiceClient InvoiceClient { get; set; }
|
||||
|
||||
private LndSwaggerClient CustomerLnd { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task GetInfo()
|
||||
{
|
||||
var res = await InvoiceClient.GetInfo();
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice()
|
||||
{
|
||||
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice()
|
||||
{
|
||||
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
|
||||
|
||||
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play()
|
||||
{
|
||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
||||
var seq2 = seq.Slice(3);
|
||||
var pos = seq2.GetPosition(0);
|
||||
}
|
||||
|
||||
// integration tests
|
||||
[Fact]
|
||||
public async Task TestWaitListenInvoice()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
var waitToken = default(CancellationToken);
|
||||
var listener = await InvoiceClient.Listen(waitToken);
|
||||
var waitTask = listener.WaitInvoice(waitToken);
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await waitTask;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
||||
|
||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice2.BOLT11
|
||||
});
|
||||
|
||||
invoice = await waitTask2;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
||||
await Task.Delay(100);
|
||||
listener.Dispose();
|
||||
Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLndInvoiceAndPay()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
|
||||
await EventuallyAsync(async () =>
|
||||
{
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var merchantNodeAddress = new LnrpcLightningAddress
|
||||
{
|
||||
Pubkey = merchantInfo.NodeId,
|
||||
Host = "merchant_lnd:9735"
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if channel is pending generate blocks until confirmed
|
||||
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
|
||||
if (pendingResponse.Pending_open_channels?
|
||||
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
|
||||
{
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if channel is established
|
||||
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
|
||||
LnrpcChannel channelToMerchant = null;
|
||||
if (chanResponse != null && chanResponse.Channels != null)
|
||||
{
|
||||
channelToMerchant = chanResponse.Channels
|
||||
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (channelToMerchant == null)
|
||||
{
|
||||
// create new channel
|
||||
var isConnected = await CustomerLnd.ListPeersAsync();
|
||||
if (isConnected.Peers == null ||
|
||||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
|
||||
{
|
||||
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
|
||||
{
|
||||
Addr = merchantNodeAddress
|
||||
});
|
||||
}
|
||||
|
||||
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
|
||||
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
|
||||
var channelReq = new LnrpcOpenChannelRequest
|
||||
{
|
||||
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
|
||||
Node_pubkey_string = merchantInfo.NodeId
|
||||
};
|
||||
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// channel exists, return it
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
return channelToMerchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await InvoiceClient.GetInfo();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
private void initializeEnvironment()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PaymentRequestTests
|
||||
{
|
||||
public PaymentRequestTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateViewUpdateAndDeletePaymentRequest()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var id = (Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||
|
||||
|
||||
|
||||
//permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(id));
|
||||
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||
|
||||
//Delete
|
||||
|
||||
Assert.IsType<ConfirmModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||
|
||||
Assert
|
||||
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(1, invoice.Price);
|
||||
|
||||
request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice with expiry",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
|
||||
response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
Assert
|
||||
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -29,11 +29,7 @@ If you want to stop, and remove all existing data
|
||||
docker-compose down --v
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
You can run tests on `MySql` database instead of `Postgres` by setting environnement variable `TESTS_DB` equals to `MySql`.
|
||||
|
||||
## How to manually test payments
|
||||
|
||||
|
@ -11,6 +11,21 @@ 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
|
||||
@ -46,8 +61,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
rules.GlobalMultiplier = 2.32m;
|
||||
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).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
|
||||
@ -104,7 +119,7 @@ namespace BTCPayServer.Tests
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
@ -113,22 +128,22 @@ namespace BTCPayServer.Tests
|
||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||
builder.AppendLine("DOGE_BTC = 2000");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rules.GlobalMultiplier = 1.1m;
|
||||
rules.Spread = 0.1m;
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
|
||||
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)) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
|
||||
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))) * 1.1", rule2.ToString());
|
||||
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))) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
|
||||
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
|
||||
@ -147,12 +162,12 @@ namespace BTCPayServer.Tests
|
||||
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.Value.Value);
|
||||
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.Value.Value);
|
||||
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
|
@ -18,10 +18,11 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -49,8 +50,8 @@ namespace BTCPayServer.Tests
|
||||
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 = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
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);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
@ -60,7 +61,9 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
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);
|
||||
@ -78,88 +81,24 @@ namespace BTCPayServer.Tests
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
||||
if (lndBackend == LightningConnectionType.LndREST)
|
||||
client = MerchantLnd.Client;
|
||||
|
||||
PrepareLightningAsync(client).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] SKIPPED_STATES =
|
||||
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
public Task EnsureChannelsSetup()
|
||||
{
|
||||
bool awaitingLocking = false;
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
|
||||
var peers = await CustomerLightningD.ListPeersAsync();
|
||||
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
|
||||
var channel = filteringToTargetedPeers
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
case null:
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
|
||||
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
|
||||
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
|
||||
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
awaitingLocking = true;
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
default:
|
||||
throw new NotSupportedException(channel?.State ?? "");
|
||||
}
|
||||
}
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
|
||||
await Task.WhenAll(synching);
|
||||
if (synching.All(c => c.Result != null))
|
||||
return synching[0].Result;
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
yield return CustomerLightningD;
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
yield return MerchantLightningD;
|
||||
yield return MerchantLnd.Client;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
@ -171,12 +110,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerLightningD.SendAsync(bolt11);
|
||||
await CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
public ILightningClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ILightningClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
@ -214,11 +153,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach(var store in Stores)
|
||||
foreach (var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ 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
|
||||
{
|
||||
@ -55,6 +59,21 @@ namespace BTCPayServer.Tests
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void SetNetworkFeeMode(NetworkFeeMode mode)
|
||||
{
|
||||
ModifyStore((store) =>
|
||||
{
|
||||
store.NetworkFeeMode = mode;
|
||||
});
|
||||
}
|
||||
public void ModifyStore(Action<StoreViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<StoresController>();
|
||||
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
|
||||
modify(store);
|
||||
storeController.UpdateStore(store).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
@ -70,25 +89,23 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
@ -132,7 +149,7 @@ namespace BTCPayServer.Tests
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
||||
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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
111
BTCPayServer.Tests/UtilitiesTests.cs
Normal file
111
BTCPayServer.Tests/UtilitiesTests.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using System.IO;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// This class hold easy to run utilities for dev time
|
||||
/// </summary>
|
||||
public class UtilitiesTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
|
||||
/// </summary>
|
||||
[Trait("Utilities", "Utilities")]
|
||||
[Fact]
|
||||
public async Task PullTransifexTranslations()
|
||||
{
|
||||
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
|
||||
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
|
||||
var client = new TransifexClient(GetTransifexAPIToken());
|
||||
var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/");
|
||||
var langs = new[] { "en" }.Concat(((JObject)json["stats"]).Properties().Select(n => n.Name)).ToArray();
|
||||
|
||||
var langsDir = Path.Combine(Services.LanguageService.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
|
||||
|
||||
JObject sourceLang = null;
|
||||
Task.WaitAll(langs.Select(async l =>
|
||||
{
|
||||
bool isSourceLang = l == "en";
|
||||
var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/");
|
||||
if(!isSourceLang)
|
||||
{
|
||||
while (sourceLang == null)
|
||||
await Task.Delay(10);
|
||||
}
|
||||
var content = j["content"].Value<string>();
|
||||
if (l == "ne_NP")
|
||||
l = "np_NP";
|
||||
if (l == "zh_CN")
|
||||
l = "zh-SP";
|
||||
if (l == "kk")
|
||||
l = "kk-KZ";
|
||||
|
||||
var langCode = l.Replace("_", "-");
|
||||
var langFile = Path.Combine(langsDir, langCode + ".json");
|
||||
var jobj = JObject.Parse(content);
|
||||
jobj["code"] = langCode;
|
||||
|
||||
if ((string)jobj["currentLanguage"] == "English" && !isSourceLang)
|
||||
return; // Not translated
|
||||
if ((string)jobj["currentLanguage"] == "disable")
|
||||
return; // Not translated
|
||||
|
||||
jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
|
||||
if (isSourceLang)
|
||||
{
|
||||
sourceLang = jobj;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(jobj["InvoiceExpired_Body_3"].Value<string>() == sourceLang["InvoiceExpired_Body_3"].Value<string>())
|
||||
{
|
||||
jobj["InvoiceExpired_Body_3"] = string.Empty;
|
||||
}
|
||||
}
|
||||
content = jobj.ToString(Newtonsoft.Json.Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(langsDir, langFile), content);
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private static string GetTransifexAPIToken()
|
||||
{
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
|
||||
var config = builder.Build();
|
||||
var token = config["TransifexAPIToken"];
|
||||
Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken <youapitoken>\"");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public class TransifexClient
|
||||
{
|
||||
public TransifexClient(string apiToken)
|
||||
{
|
||||
Client = new HttpClient();
|
||||
APIToken = apiToken;
|
||||
}
|
||||
|
||||
public HttpClient Client { get; }
|
||||
public string APIToken { get; }
|
||||
|
||||
public async Task<JObject> GetTransifexAsync(string uri)
|
||||
{
|
||||
var message = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes($"api:{APIToken}")));
|
||||
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
var response = await Client.SendAsync(message);
|
||||
return await response.Content.ReadAsAsync<JObject>();
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
|
||||
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
|
||||
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
|
||||
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
|
||||
docker exec -ti "$bitcoind_container_id" bitcoin-cli -datadir="/data" "$@"
|
||||
|
@ -2,7 +2,7 @@ version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
tests:
|
||||
@ -14,13 +14,15 @@ services:
|
||||
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_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
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"
|
||||
@ -34,14 +36,16 @@ services:
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
@ -49,21 +53,24 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- mysql
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.14
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -86,32 +93,35 @@ services:
|
||||
- bitcoind
|
||||
- litecoind
|
||||
|
||||
|
||||
bitcoind:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawtx=tcp://0.0.0.0:28332
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtxlock=tcp://0.0.0.0:28332
|
||||
zmqpubhashblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
deprecatedrpc=signrawtransaction
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "28332" # ZMQ
|
||||
- "28333" # ZMQ
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -122,6 +132,7 @@ services:
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
@ -134,7 +145,8 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
image: shesek/lightning-charge:0.4.6-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -153,7 +165,8 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -176,13 +189,13 @@ services:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
image: nicolasdorier/docker-litecoin:0.15.1
|
||||
restart: unless-stopped
|
||||
image: nicolasdorier/docker-litecoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
server=1
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
@ -198,21 +211,36 @@ services:
|
||||
- "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:0.4.2.0
|
||||
image: btcpayserver/lnd:v0.5.2-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.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
@ -224,19 +252,25 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
image: btcpayserver/lnd:v0.5.2-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.zmqpath=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
|
||||
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
|
||||
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli "$@"
|
||||
|
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
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
|
||||
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
|
||||
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
|
||||
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
|
||||
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"
|
||||
|
@ -1 +1,2 @@
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
|
||||
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli $args
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
|
||||
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"
|
||||
|
5
BTCPayServer.Tests/xunit.runner.json
Normal file
5
BTCPayServer.Tests/xunit.runner.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"parallelizeTestCollections": false,
|
||||
"longRunningTestSeconds": 60,
|
||||
"diagnosticMessages": true
|
||||
}
|
@ -242,6 +242,8 @@ namespace BTCPayServer.Authentication
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if (token == null)
|
||||
return null;
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ namespace BTCPayServer
|
||||
"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'")
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = zaif(MONA_BTC)"
|
||||
"MONA_BTC = bittrex(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
|
@ -47,13 +47,18 @@ namespace BTCPayServer
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitBitcore();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitPolis();
|
||||
InitDash();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,12 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.68</Version>
|
||||
<Version>1.0.3.77</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\**" />
|
||||
@ -30,33 +33,42 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0" />
|
||||
<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.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.32" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
|
||||
<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.84" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.32" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.4" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<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.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -113,10 +125,39 @@
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Server\LNDGRPCServices.cshtml">
|
||||
<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\LightningWalletServices.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">
|
||||
@ -125,11 +166,26 @@
|
||||
<Content Update="Views\Server\Services.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<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>
|
||||
|
@ -11,6 +11,11 @@ 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 Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -32,6 +37,12 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string LogFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DataDir
|
||||
{
|
||||
get;
|
||||
@ -49,6 +60,22 @@ namespace BTCPayServer.Configuration
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
public bool DisableRegistration
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public static string GetDebugLog(IConfiguration configuration)
|
||||
{
|
||||
return configuration.GetValue<string>("debuglog", null);
|
||||
}
|
||||
public static LogEventLevel GetDebugLogLevel(IConfiguration configuration)
|
||||
{
|
||||
var raw = configuration.GetValue("debugloglevel", nameof(LogEventLevel.Debug));
|
||||
return (LogEventLevel)Enum.Parse(typeof(LogEventLevel), raw, true);
|
||||
}
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
@ -74,59 +101,155 @@ namespace BTCPayServer.Configuration
|
||||
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 lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.external.lnd.grpc", string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.lnd.grpc, " + Environment.NewLine +
|
||||
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type=lnd-grpc;server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalLNDGRPC(connectionString));
|
||||
}
|
||||
}
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
var services = conf.GetOrDefault<string>("externalservices", null);
|
||||
if (services != null)
|
||||
{
|
||||
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(p => p.SeparatorIndex != -1)
|
||||
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
|
||||
Link: p.p.Substring(p.SeparatorIndex + 1))))
|
||||
{
|
||||
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
|
||||
OtherExternalServices.AddOrReplace(service.Name, uri);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
|
||||
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 should not be used anymore, use btclightning instead");
|
||||
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
|
||||
|
||||
LogFile = GetDebugLog(conf);
|
||||
if (!string.IsNullOrEmpty(LogFile))
|
||||
{
|
||||
Logs.Configuration.LogInformation("LogFile: " + LogFile);
|
||||
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
}
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var 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";
|
||||
}
|
||||
}
|
||||
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 ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -134,7 +257,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
public string MySQLConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@ -144,39 +267,11 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class ExternalLNDGRPC : ExternalService
|
||||
{
|
||||
public ExternalLNDGRPC(LightningConnectionString connectionString)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
else if (typeof(T) == typeof(string))
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPAddress))
|
||||
return (T)(object)IPAddress.Parse(str);
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
|
@ -31,9 +31,19 @@ namespace BTCPayServer.Configuration
|
||||
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();
|
||||
@ -41,6 +51,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externalspark", $"Show spark information in Server settings / Server. The connection string to spark server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externalcharge", $"Show lightning charge information in Server settings/Server. The connection string to charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
@ -99,9 +111,12 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine("#httpscertificatefilepath=devtest.pfx");
|
||||
builder.AppendLine("#httpscertificatefilepassword=toto");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
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())
|
||||
|
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
192
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
80
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
@ -13,7 +14,7 @@ using System.Threading.Tasks;
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
[BitpayAPIConstraint()]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
|
@ -17,6 +17,8 @@ using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -26,10 +28,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -37,15 +40,17 @@ namespace BTCPayServer.Controllers
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository)
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
@ -69,6 +74,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
@ -87,7 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
// 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);
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
@ -236,23 +242,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Register(string returnUrl = null)
|
||||
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription)
|
||||
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)
|
||||
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)
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
@ -266,15 +274,24 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
|
||||
if(_Options.DisableRegistration)
|
||||
{
|
||||
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
|
||||
policies.LockSubscription = true;
|
||||
await _SettingsRepository.UpdateSetting(policies);
|
||||
}
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
RegisteredUserId = user.Id;
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
if(logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
@ -430,8 +447,9 @@ namespace BTCPayServer.Controllers
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
|
144
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
144
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
@ -0,0 +1,144 @@
|
||||
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 AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public object Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
PerksTemplate = settings.PerksTemplate,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = app.TagAllInvoices,
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
|
||||
}
|
||||
|
||||
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var newSettings = new CrowdfundSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate,
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _htmlSanitizer.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
SoundsEnabled = vm.SoundsEnabled,
|
||||
DisqusShortname = vm.DisqusShortname,
|
||||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
|
||||
_EventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,14 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Newtonsoft.Json;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using System.IO;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Globalization;
|
||||
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
|
||||
{
|
||||
@ -28,28 +18,65 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "My awesome Point of Sale";
|
||||
Title = "Tea shop";
|
||||
Currency = "USD";
|
||||
Template =
|
||||
"tea:\n" +
|
||||
" price: 0.02\n" +
|
||||
" title: Green Tea # title is optional, defaults to the keys\n\n" +
|
||||
"coffee:\n" +
|
||||
" price: 1\n\n" +
|
||||
"bamba:\n" +
|
||||
" price: 3\n\n" +
|
||||
"beer:\n" +
|
||||
" price: 7\n\n" +
|
||||
"hat:\n" +
|
||||
" price: 15\n\n" +
|
||||
"tshirt:\n" +
|
||||
" price: 25";
|
||||
"green tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Green Tea\n" +
|
||||
" description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2015/03/26/11/03/green-tea-692339__480.jpg\n\n" +
|
||||
"black tea:\n" +
|
||||
" price: 1\n" +
|
||||
" title: Black Tea\n" +
|
||||
" description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2016/11/29/13/04/beverage-1869716__480.jpg\n\n" +
|
||||
"rooibos:\n" +
|
||||
" price: 1.2\n" +
|
||||
" title: Rooibos\n" +
|
||||
" description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2017/01/08/08/14/water-1962388__480.jpg\n\n" +
|
||||
"pu erh:\n" +
|
||||
" price: 2\n" +
|
||||
" title: Pu Erh\n" +
|
||||
" description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2018/07/21/16/56/tea-cup-3552917__480.jpg\n\n" +
|
||||
"herbal tea:\n" +
|
||||
" price: 1.8\n" +
|
||||
" title: Herbal Tea\n" +
|
||||
" description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2015/07/02/20/57/chamomile-829538__480.jpg\n" +
|
||||
" custom: true\n\n" +
|
||||
"fruit tea:\n" +
|
||||
" price: 1.5\n" +
|
||||
" title: Fruit Tea\n" +
|
||||
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
|
||||
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
|
||||
" custom: true";
|
||||
EnableShoppingCart = false;
|
||||
ShowCustomAmount = true;
|
||||
ShowDiscount = true;
|
||||
EnableTips = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
|
||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
|
||||
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
||||
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
|
||||
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
|
||||
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
|
||||
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -62,10 +89,19 @@ namespace BTCPayServer.Controllers
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Id = appId,
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template
|
||||
Template = settings.Template,
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
@ -86,7 +122,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = Parse(settings.Template, settings.Currency);
|
||||
var items = _AppService.Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
@ -108,11 +144,11 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
Parse(vm.Template, vm.Currency);
|
||||
_AppService.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -128,147 +164,23 @@ namespace BTCPayServer.Controllers
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await 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)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await GetStore(app);
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
||||
private async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
@ -276,8 +188,25 @@ namespace BTCPayServer.Controllers
|
||||
ctx.Apps.Add(app);
|
||||
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
||||
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
|
||||
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,61 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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 BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class AppsController : Controller
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
CurrencyNameTable _Currencies;
|
||||
InvoiceController _InvoiceController;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
InvoiceController invoiceController)
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
AppService AppService)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_AppService = AppService;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private AppService _AppService;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
var apps = await _AppService.GetAllApps(GetUserId());
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
@ -55,7 +69,7 @@ namespace BTCPayServer.Controllers
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await DeleteApp(appData))
|
||||
if (await _AppService.DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
@ -64,10 +78,15 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppService.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must have created at least one store";
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You 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();
|
||||
@ -79,10 +98,15 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppService.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must own at least one store";
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You 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;
|
||||
@ -102,7 +126,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var appData = new AppData() { Id = id };
|
||||
@ -113,10 +137,18 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
CreatedAppId = id;
|
||||
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.PointOfSale:
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
case AppType.Crowdfund:
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
|
||||
default:
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -134,67 +166,12 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DeleteApp(AppData appData)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
278
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
278
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
@ -0,0 +1,278 @@
|
||||
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.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppService AppService,
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppService = AppService;
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppService _AppService;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
|
||||
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
|
||||
{
|
||||
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
|
||||
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
|
||||
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
|
||||
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
|
||||
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
|
||||
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
|
||||
},
|
||||
Items = _AppService.Parse(settings.Template, settings.Currency),
|
||||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
CustomTipPercentages = settings.CustomTipPercentages,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
AppId = appId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
|
||||
if(!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _AppService.GetAppInfo(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 _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(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 _AppService.GetStore(app);
|
||||
var title = settings.Title;
|
||||
var price = request.Amount;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
if (request.Amount > price)
|
||||
price = request.Amount;
|
||||
}
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
{
|
||||
return NotFound("Contribution Amount is more than is currently allowed.");
|
||||
}
|
||||
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
|
||||
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 _AppService.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 = _AppService.Parse(settings.Template, settings.Currency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = choice.Price.Value;
|
||||
if (amount > price)
|
||||
price = amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
var store = await _AppService.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl ?? 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);
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -6,16 +6,85 @@ 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,26 +1,19 @@
|
||||
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.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
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
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
@ -39,23 +32,27 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
|
||||
{
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = id,
|
||||
StoreId = new[] { HttpContext.GetStoreData().Id }
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
@ -70,15 +67,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
Skip = offset,
|
||||
EndDate = dateEnd,
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
OrderId = orderId == null ? null : new[] { orderId },
|
||||
ItemCode = itemCode == null ? null : new[] { itemCode },
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
@ -1,87 +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;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
|
||||
if (store.StoreCertificate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentRequestActionResult(request);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}", Order = 99)]
|
||||
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +1,31 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
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.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Net.Mime;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
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
|
||||
{
|
||||
@ -30,12 +33,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId(),
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
@ -44,13 +48,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
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,
|
||||
Status = invoice.Status,
|
||||
State = invoice.GetInvoiceState().ToString(),
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
@ -61,12 +64,15 @@ namespace BTCPayServer.Controllers
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
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
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(dto.PosData)
|
||||
};
|
||||
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
@ -76,9 +82,9 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (onchainMethod != null)
|
||||
@ -100,7 +106,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
@ -176,17 +182,20 @@ namespace BTCPayServer.Controllers
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null,
|
||||
[FromQuery]string view = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
@ -204,37 +213,37 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
bool isDefaultPaymentId = false;
|
||||
if (paymentMethodId == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto();
|
||||
isDefaultCrypto = true;
|
||||
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
isDefaultPaymentId = true;
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
if (network == null && isDefaultPaymentId)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if (!isDefaultCrypto)
|
||||
if (!isDefaultPaymentId)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
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();
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
@ -244,6 +253,23 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
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;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
(1m + (changelly.AmountMarkupPercentage / 100m)))
|
||||
: (decimal?)null;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
@ -251,10 +277,9 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
@ -280,10 +305,18 @@ namespace BTCPayServer.Controllers
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
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()
|
||||
@ -307,7 +340,7 @@ namespace BTCPayServer.Controllers
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " (via Lightning)";
|
||||
network.DisplayName : network.DisplayName + " (Lightning)";
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
@ -323,47 +356,23 @@ namespace BTCPayServer.Controllers
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||
}
|
||||
|
||||
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||
{
|
||||
var provider = currencies.GetNumberFormatInfo(currency, true);
|
||||
var currencyData = currencies.GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
||||
{
|
||||
price = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return price.ToString("C", provider);
|
||||
else
|
||||
return price.ToString("C", provider) + $" ({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);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -371,12 +380,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status/ws")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
|
||||
[Route("invoice/{invoiceId}/status/ws")]
|
||||
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
|
||||
[Route("invoice/status/ws")]
|
||||
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -416,6 +429,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
[Route("invoice/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -432,40 +446,83 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
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);
|
||||
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
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
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
ShowCheckout = invoice.Status == "new",
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
model.Skip = skip;
|
||||
model.Count = count;
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
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)]
|
||||
@ -499,7 +556,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
StatusMessage = null;
|
||||
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
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);
|
||||
@ -521,13 +578,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
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,
|
||||
@ -557,17 +615,60 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
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();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
@ -582,5 +683,49 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
public class PosDataParser
|
||||
{
|
||||
public static Dictionary<string, object> ParsePosData(string posData)
|
||||
{
|
||||
var result = new Dictionary<string,object>();
|
||||
if (string.IsNullOrEmpty(posData))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jObject =JObject.Parse(posData);
|
||||
foreach (var item in jObject)
|
||||
{
|
||||
|
||||
switch (item.Value.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
var items = item.Value.AsEnumerable().ToList();
|
||||
for (var i = 0; i < items.Count(); i++)
|
||||
{
|
||||
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
|
||||
}
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
result.Add(item.Key, ParsePosData(item.Value.ToString()));
|
||||
break;
|
||||
default:
|
||||
result.Add(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.Add(string.Empty, posData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,27 @@
|
||||
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.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -49,7 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ContentSecurityPolicies _CSP;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
RateFetcher _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
@ -62,7 +42,7 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayRateProviderFactory rateProvider,
|
||||
RateFetcher rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
@ -82,19 +62,20 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
@ -102,9 +83,17 @@ namespace BTCPayServer.Controllers
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
if (invoice.NotificationURL != null &&
|
||||
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
|
||||
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
|
||||
{
|
||||
entity.NotificationURL = notificationUri.AbsoluteUri;
|
||||
}
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
if (additionalTags != null)
|
||||
entity.InternalTags.AddRange(additionalTags);
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -113,19 +102,48 @@ namespace BTCPayServer.Controllers
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
int divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
|
||||
divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
|
||||
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
|
||||
|
||||
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
|
||||
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
|
||||
{
|
||||
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
}
|
||||
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.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));
|
||||
@ -140,6 +158,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
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,
|
||||
@ -163,7 +182,7 @@ namespace BTCPayServer.Controllers
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error.ToString());
|
||||
@ -174,9 +193,15 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
|
||||
foreach (var app in await getAppsTaggingStore)
|
||||
{
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
}
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
@ -193,12 +218,9 @@ namespace BTCPayServer.Controllers
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
if (rateResult.ExchangeExceptions.Count != 0)
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
@ -208,8 +230,9 @@ namespace BTCPayServer.Controllers
|
||||
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.Value == null)
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -217,10 +240,8 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.Value.Value;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
@ -244,9 +265,9 @@ namespace BTCPayServer.Controllers
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||
if (limitValueRate.Value.HasValue)
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
@ -260,7 +281,7 @@ namespace BTCPayServer.Controllers
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.TxFee = paymentMethod.NextNetworkFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
|
68
BTCPayServer/Controllers/Macaroons.cs
Normal file
68
BTCPayServer/Controllers/Macaroons.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class Macaroons
|
||||
{
|
||||
public class Macaroon
|
||||
{
|
||||
public Macaroon(byte[] bytes)
|
||||
{
|
||||
Bytes = bytes;
|
||||
Hex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(bytes);
|
||||
}
|
||||
|
||||
public string Hex { get; set; }
|
||||
public byte[] Bytes { get; set; }
|
||||
}
|
||||
public static async Task<Macaroons> GetFromDirectoryAsync(string directoryPath)
|
||||
{
|
||||
if (directoryPath == null)
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (Path.GetFileName(file))
|
||||
{
|
||||
case "admin.macaroon":
|
||||
macaroons.AdminMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
case "readonly.macaroon":
|
||||
macaroons.ReadonlyMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
case "invoice.macaroon":
|
||||
macaroons.InvoiceMacaroon = new Macaroon(await File.ReadAllBytesAsync(file));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
public Macaroon AdminMacaroon { get; set; }
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
|
||||
public ManageController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
ILogger<ManageController> logger,
|
||||
UrlEncoder urlEncoder,
|
||||
TokenRepository tokenRepository,
|
||||
@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
var email = user.Email;
|
||||
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
|
||||
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
|
||||
StatusMessage = "Verification email sent. Please check your email.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
@ -443,7 +442,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError("model.Code", "Verification code is invalid.");
|
||||
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
321
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
321
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("payment-requests")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public class PaymentRequestController : Controller
|
||||
{
|
||||
private readonly InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
|
||||
public PaymentRequestController(
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
{
|
||||
UserId = GetUserId(), Skip = skip, Count = count
|
||||
});
|
||||
return View(new ListPaymentRequestsViewModel()
|
||||
{
|
||||
Skip = skip,
|
||||
StatusMessage = statusMessage,
|
||||
Count = count,
|
||||
Total = result.Total,
|
||||
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
SelectList stores = null;
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
if (!stores.Any())
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a payment request"
|
||||
});
|
||||
}
|
||||
|
||||
return View(new UpdatePaymentRequestViewModel(data)
|
||||
{
|
||||
Stores = stores,
|
||||
StatusMessage = statusMessage
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(viewModel.Currency) ||
|
||||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
|
||||
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
|
||||
nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
data = new PaymentRequestData();
|
||||
}
|
||||
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||
blob.Currency = viewModel.Currency;
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
|
||||
data.SetBlob(blob);
|
||||
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||
_EventAggregator.Publish(new PaymentRequestUpdated()
|
||||
{
|
||||
Data = data,
|
||||
PaymentRequestId = data.Id
|
||||
});
|
||||
|
||||
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
|
||||
{
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blob = data.GetBlob();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
|
||||
if (result)
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new {StatusMessage = "Payment request successfully removed"});
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage =
|
||||
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has already been settled.");
|
||||
}
|
||||
|
||||
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has expired");
|
||||
}
|
||||
|
||||
var statusesAllowedToDisplay = new List<InvoiceStatus>()
|
||||
{
|
||||
InvoiceStatus.New
|
||||
};
|
||||
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
|
||||
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
|
||||
statusesAllowedToDisplay.Contains(status));
|
||||
|
||||
if (validInvoice != null)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
|
||||
}
|
||||
|
||||
return Ok(validInvoice.Id);
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
{
|
||||
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
|
||||
}
|
||||
|
||||
|
||||
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
|
||||
bool redirectToInvoice,
|
||||
ViewPaymentRequestViewModel result,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
|
||||
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.GetValueOrDefault(result.AmountDue),
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
|
||||
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
|
||||
}
|
||||
|
||||
return Ok(newInvoiceId);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
63
BTCPayServer/Controllers/PublicController.cs
Normal file
63
BTCPayServer/Controllers/PublicController.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
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 CreateInvoiceRequest()
|
||||
{
|
||||
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,89 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
||||
[Route("embed/{storeId}/{cryptoCode}/ln")]
|
||||
[AllowAnonymous]
|
||||
public class PublicLightningNodeInfoController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
|
||||
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
|
||||
{
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_LightningLikePaymentHandler = lightningLikePaymentHandler;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(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; }
|
||||
}
|
||||
}
|
@ -10,33 +10,87 @@ 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
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class RateController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
RateFetcher _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
|
||||
public TokenRepository TokenRepository { get; }
|
||||
|
||||
public RateController(
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
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 = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
@ -44,19 +98,37 @@ namespace BTCPayServer.Controllers
|
||||
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)
|
||||
{
|
||||
if(storeId == null || currencyPairs == null)
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
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)
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
@ -64,12 +136,30 @@ namespace BTCPayServer.Controllers
|
||||
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(','))
|
||||
foreach (var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if(!CurrencyPair.TryParse(currency, out var pair))
|
||||
if (!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
@ -81,7 +171,7 @@ namespace BTCPayServer.Controllers
|
||||
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().Value))
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
@ -94,6 +184,20 @@ namespace BTCPayServer.Controllers
|
||||
}).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
|
||||
{
|
||||
|
||||
|
@ -8,7 +8,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validations;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@ -24,6 +25,8 @@ using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -32,21 +35,26 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
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;
|
||||
@ -157,16 +165,19 @@ namespace BTCPayServer.Controllers
|
||||
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))
|
||||
@ -175,6 +186,8 @@ namespace BTCPayServer.Controllers
|
||||
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");
|
||||
@ -195,12 +208,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
builder.Scheme = this.Request.Scheme;
|
||||
builder.Host = vm.DNSDomain;
|
||||
if (this.Request.Host.Port != null)
|
||||
builder.Port = this.Request.Host.Port.Value;
|
||||
builder.Path = "runid";
|
||||
builder.Query = $"expected={RunId}";
|
||||
var response = await client.GetAsync(builder.Uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
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);
|
||||
@ -217,7 +231,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
var error = RunSSH(vm, command, $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && . changedomain.sh {vm.DNSDomain}'");
|
||||
var error = RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
|
||||
if (error != null)
|
||||
return error;
|
||||
|
||||
@ -227,7 +241,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var error = RunSSH(vm, command, $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && btcpay-update.sh'");
|
||||
var error = RunSSH(vm, $"btcpay-update.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
@ -239,6 +253,13 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(Maintenance));
|
||||
}
|
||||
|
||||
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
|
||||
{
|
||||
if (IPAddress.TryParse(domainOrIP, out var ip))
|
||||
return Task.FromResult(new[] { ip });
|
||||
return Dns.GetHostAddressesAsync(domainOrIP);
|
||||
}
|
||||
|
||||
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
|
||||
[HttpGet]
|
||||
[Route("runid")]
|
||||
@ -250,9 +271,34 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
private IActionResult RunSSH(MaintenanceViewModel vm, string command, string ssh)
|
||||
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
|
||||
{
|
||||
var sshClient = vm.CreateSSHClient(this.Request.Host.Host);
|
||||
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();
|
||||
@ -281,7 +327,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation("Running SSH command: " + command);
|
||||
Logs.PayServer.LogInformation("Running SSH command: " + ssh);
|
||||
var result = sshCommand.EndExecute(ar);
|
||||
Logs.PayServer.LogInformation("SSH command executed: " + result);
|
||||
}
|
||||
@ -306,22 +352,27 @@ namespace BTCPayServer.Controllers
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var isAdmin = IsAdmin(roles);
|
||||
bool updated = false;
|
||||
|
||||
if (isAdmin != viewModel.IsAdmin)
|
||||
viewModel.StatusMessage = "";
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (!viewModel.IsAdmin && admins.Count == 1)
|
||||
{
|
||||
viewModel.StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added.";
|
||||
return View(viewModel); // return
|
||||
}
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (viewModel.IsAdmin != IsAdmin(roles))
|
||||
{
|
||||
if (viewModel.IsAdmin)
|
||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
else
|
||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||
updated = true;
|
||||
}
|
||||
if (updated)
|
||||
{
|
||||
|
||||
viewModel.StatusMessage = "User successfully updated";
|
||||
}
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
@ -332,12 +383,29 @@ namespace BTCPayServer.Controllers
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
Title = "Delete user " + user.Email,
|
||||
Description = "This user will be permanently deleted",
|
||||
Action = "Delete"
|
||||
});
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (IsAdmin(roles))
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||
"Delete"));
|
||||
}
|
||||
else
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
|
||||
"This user will be permanently deleted",
|
||||
"Delete"));
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
@ -358,6 +426,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
@ -385,60 +454,128 @@ namespace BTCPayServer.Controllers
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
result.ExternalServices = _Options.ExternalServices;
|
||||
foreach (var externalService in _Options.OtherExternalServices)
|
||||
{
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "gRPC",
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (_Options.SSHSettings != null)
|
||||
{
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
var model = new LNDGRPCServicesViewModel();
|
||||
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
if (external.CertificateThumbprint != null)
|
||||
try
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
switch (service.Type)
|
||||
{
|
||||
case ExternalServiceTypes.Charge:
|
||||
return LightningChargeServices(service, connectionString, showQR);
|
||||
case ExternalServiceTypes.RTL:
|
||||
case ExternalServiceTypes.Spark:
|
||||
if (connectionString.AccessKey == null)
|
||||
{
|
||||
StatusMessage = $"Error: The access key of the service is not set";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||
{
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||
vm.APIToken = connectionString.APIToken;
|
||||
var builder = new UriBuilder(connectionString.Server);
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||
{
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
|
||||
model.SSL = connectionString.Server.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (connectionString.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = connectionString.CertificateThumbprint;
|
||||
}
|
||||
if (connectionString.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
}
|
||||
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCodeLink = Url.Action(nameof(GetLNDConfig), new { configKey = configKey });
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return View(nameof(LndServices), model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
@ -451,50 +588,92 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
[HttpPost]
|
||||
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
|
||||
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
|
||||
{
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var service = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
|
||||
ExternalConnectionString connectionString = null;
|
||||
try
|
||||
{
|
||||
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = connectionString.Server.DnsSafeHost;
|
||||
grpcConf.Port = connectionString.Server.Port;
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
|
||||
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
|
||||
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
|
||||
[Route("server/services/ssh")]
|
||||
public IActionResult SSHService(bool downloadKeyFile = false)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
var settings = _Options.SSHSettings;
|
||||
if (settings == null)
|
||||
return NotFound();
|
||||
if (downloadKeyFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
if (!System.IO.File.Exists(settings.KeyFile))
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
return connectionString;
|
||||
|
||||
var server = IsLocalNetwork(settings.Server) ? this.Request.Host.Host: settings.Server;
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static bool IsLocalNetwork(string server)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
@ -542,5 +721,67 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/logs/{file?}")]
|
||||
public async Task<IActionResult> LogsView(string file = null, int offset = 0)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
var vm = new LogsViewModel();
|
||||
|
||||
if (string.IsNullOrEmpty(_Options.LogFile))
|
||||
{
|
||||
vm.StatusMessage = "Error: File Logging Option not specified. " +
|
||||
"You need to set debuglog and optionally " +
|
||||
"debugloglevel in the configuration or through runtime arguments";
|
||||
}
|
||||
else
|
||||
{
|
||||
var di = Directory.GetParent(_Options.LogFile);
|
||||
if (di == null)
|
||||
{
|
||||
vm.StatusMessage = "Error: Could not load log files";
|
||||
}
|
||||
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_Options.LogFile);
|
||||
var fileExtension = Path.GetExtension(_Options.LogFile) ?? string.Empty;
|
||||
var logFiles = di.GetFiles($"{fileNameWithoutExtension}*{fileExtension}");
|
||||
vm.LogFileCount = logFiles.Length;
|
||||
vm.LogFiles = logFiles
|
||||
.OrderBy(info => info.LastWriteTime)
|
||||
.Skip(offset)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
vm.LogFileOffset = offset;
|
||||
|
||||
if (string.IsNullOrEmpty(file))
|
||||
return View("Logs", vm);
|
||||
vm.Log = "";
|
||||
var path = Path.Combine(di.FullName, file);
|
||||
try
|
||||
{
|
||||
using (var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
vm.Log = await reader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
return View("Logs", vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,16 +34,73 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
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)
|
||||
@ -59,7 +116,6 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
@ -78,6 +134,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@ -93,11 +154,41 @@ namespace BTCPayServer.Controllers
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
if (!vm.Confirmation && strategy != null)
|
||||
return ShowAddresses(vm, strategy);
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
|
||||
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
|
||||
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
|
||||
@ -123,26 +214,8 @@ namespace BTCPayServer.Controllers
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
}
|
||||
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 });
|
||||
}
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
@ -161,279 +234,5 @@ namespace BTCPayServer.Controllers
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/ws/ledger")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string storeId,
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
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, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
||||
result = getxpubResult;
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
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");
|
||||
}
|
||||
|
||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var c in unspentCoins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = _ExplorerProvider.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(strategyBase);
|
||||
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.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(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = GetDerivationStrategy(store, network);
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
||||
}
|
||||
|
||||
return strategy.DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,12 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -24,9 +24,12 @@ namespace BTCPayServer.Controllers
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
@ -34,6 +37,7 @@ namespace BTCPayServer.Controllers
|
||||
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)
|
||||
{
|
||||
@ -111,7 +115,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
@ -134,39 +138,41 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
|
||||
if (command == "save")
|
||||
switch (command)
|
||||
{
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else // if(command == "test")
|
||||
{
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
case "test":
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,18 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -11,6 +20,7 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -18,14 +28,6 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -35,50 +37,49 @@ namespace BTCPayServer.Controllers
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
BTCPayRateProviderFactory _RateFactory;
|
||||
RateFetcher _RateFactory;
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
StoreRepository repo,
|
||||
TokenRepository tokenRepo,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayRateProviderFactory rateFactory,
|
||||
RateFetcher rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env)
|
||||
ChangellyClientProvider changellyClientProvider,
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
IHostingEnvironment env, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
MvcJsonOptions = mvcJsonOptions;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
}
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private ExplorerClientProvider _ExplorerProvider;
|
||||
private MvcJsonOptions _MvcJsonOptions;
|
||||
private IFeeProviderFactory _FeeRateProvider;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
AccessTokenController _TokenController;
|
||||
@ -86,27 +87,19 @@ namespace BTCPayServer.Controllers
|
||||
TokenRepository _TokenRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private LanguageService _LangService;
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
IHostingEnvironment _Env;
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public IActionResult Wallet(string cryptoCode)
|
||||
[TempData]
|
||||
public bool StoreNotConfigured
|
||||
{
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -179,7 +172,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove access to {user.Email}?",
|
||||
Description = $"Are you sure you want to remove store access for {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
@ -200,7 +193,7 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
@ -225,7 +218,7 @@ namespace BTCPayServer.Controllers
|
||||
model.AvailableExchanges = GetSupportedExchanges();
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
@ -283,7 +276,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CurrencyPair = fetch.Key.ToString(),
|
||||
Error = testResult.Errors.Count != 0,
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
@ -338,17 +331,33 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
|
||||
SetCryptoCurrencies(vm, StoreData);
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
return View(vm);
|
||||
}
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
{
|
||||
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
|
||||
.Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray();
|
||||
|
||||
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
|
||||
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
|
||||
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.DefaultPaymentMethod = chosen?.Value;
|
||||
}
|
||||
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
display : $"{display} (Lightning)";
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
@ -373,12 +382,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
bool needUpdate = false;
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
|
||||
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
|
||||
{
|
||||
needUpdate = true;
|
||||
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
SetCryptoCurrencies(model, StoreData);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -386,7 +396,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
@ -422,10 +431,11 @@ namespace BTCPayServer.Controllers
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
|
||||
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, vm);
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
@ -434,8 +444,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
|
||||
{
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
@ -447,7 +458,9 @@ namespace BTCPayServer.Controllers
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
|
||||
});
|
||||
}
|
||||
|
||||
@ -459,12 +472,31 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
|
||||
var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike);
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
|
||||
Enabled = !excludeFilters.Match(paymentId)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = changellyEnabled,
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = coinSwitchEnabled,
|
||||
Action = nameof(UpdateCoinSwitchSettings),
|
||||
Provider = "CoinSwitch"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -489,7 +521,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
@ -537,7 +570,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return _RateFactory.GetSupportedExchanges()
|
||||
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
@ -557,6 +590,7 @@ namespace BTCPayServer.Controllers
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
|
||||
model.StatusMessage = StatusMessage;
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Facade = t.Facade,
|
||||
@ -573,6 +607,45 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}/revoke")]
|
||||
public async Task<IActionResult> RevokeToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null || token.StoreId != StoreData.Id)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Revoke the token",
|
||||
Title = "Revoke the token",
|
||||
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
|
||||
ButtonClass = "btn-danger"
|
||||
});
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/{tokenId}/revoke")]
|
||||
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
StatusMessage = "Token revoked";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}")]
|
||||
public async Task<IActionResult> ShowToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null || token.StoreId != StoreData.Id)
|
||||
return NotFound();
|
||||
return View(token);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
@ -637,6 +710,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
public string GeneratedPairingCode { get; set; }
|
||||
public IOptions<MvcJsonOptions> MvcJsonOptions { get; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
@ -674,21 +748,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/Tokens/Delete")]
|
||||
public async Task<IActionResult> DeleteToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
if (token == null ||
|
||||
token.StoreId != StoreData.Id ||
|
||||
!await _TokenRepository.DeleteToken(tokenId))
|
||||
StatusMessage = "Failure to revoke this token";
|
||||
else
|
||||
StatusMessage = "Token revoked";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey()
|
||||
@ -759,12 +818,17 @@ namespace BTCPayServer.Controllers
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(p => !excludeFilter.Match(p.PaymentId))
|
||||
.Count() == 0;
|
||||
StatusMessage = "Pairing is successful";
|
||||
if (pairingResult == PairingResult.Partial)
|
||||
StatusMessage = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
storeId = store.Id,
|
||||
pairingCode = pairingCode
|
||||
});
|
||||
}
|
||||
else
|
||||
@ -783,5 +847,54 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: Need to have talk about how architect default currency implementation
|
||||
// For now we have also hardcoded USD for Store creation and then Invoice creation
|
||||
const string DEFAULT_CURRENCY = "USD";
|
||||
|
||||
[Route("{storeId}/paybutton")]
|
||||
public IActionResult PayButton()
|
||||
{
|
||||
var store = StoreData;
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
{
|
||||
return View("PayButtonEnable", null);
|
||||
}
|
||||
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||
var model = new PayButtonViewModel
|
||||
{
|
||||
Price = 10,
|
||||
Currency = DEFAULT_CURRENCY,
|
||||
ButtonSize = 2,
|
||||
UrlRoot = appUrl,
|
||||
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
|
||||
StoreId = store.Id
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/paybutton")]
|
||||
public async Task<IActionResult> PayButton(bool enableStore)
|
||||
{
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = enableStore;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(StoreData);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(PayButton), new
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,18 +23,15 @@ namespace BTCPayServer.Controllers
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private BTCPayWalletProvider _WalletProvider;
|
||||
|
||||
public UserStoresController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_Repo = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -85,17 +82,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
|
||||
var balances = stores
|
||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
@ -104,8 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
||||
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
@ -128,22 +113,6 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
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; }
|
||||
}
|
||||
}
|
12
BTCPayServer/CorsPolicies.cs
Normal file
12
BTCPayServer/CorsPolicies.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class CorsPolicies
|
||||
{
|
||||
public const string All = "BTCPAY_ALL";
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool TagAllInvoices { get; set; }
|
||||
public string Settings { get; set; }
|
||||
|
||||
public T GetSettings<T>() where T : class, new()
|
||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@ -54,6 +55,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PaymentRequests)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasIndex(o => o.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
@ -17,7 +14,8 @@ namespace BTCPayServer.Data
|
||||
public enum DatabaseType
|
||||
{
|
||||
Sqlite,
|
||||
Postgres
|
||||
Postgres,
|
||||
MySQL,
|
||||
}
|
||||
public class ApplicationDbContextFactory
|
||||
{
|
||||
@ -95,16 +93,8 @@ namespace BTCPayServer.Data
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
{
|
||||
builder.UseMemoryStorage();
|
||||
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||
//if (_Type == DatabaseType.Sqlite)
|
||||
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||
//else if (_Type == DatabaseType.Postgres)
|
||||
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
else if (_Type == DatabaseType.MySQL)
|
||||
builder.UseMySql(_ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,5 +81,10 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
|
||||
public Services.Invoices.InvoiceState GetInvoiceState()
|
||||
{
|
||||
return new Services.Invoices.InvoiceState(Status, ExceptionStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,12 @@ using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -38,6 +42,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
@ -53,7 +62,6 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
@ -166,24 +174,25 @@ namespace BTCPayServer.Data
|
||||
public Claim[] GetClaims()
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.AddRange(AdditionalClaims);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
var role = Role;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
if (role == StoreRoles.Owner)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
}
|
||||
if (role == StoreRoles.Guest)
|
||||
|
||||
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
|
||||
}
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
public bool HasClaim(string claim)
|
||||
{
|
||||
return GetClaims().Any(c => c.Type == claim);
|
||||
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
|
||||
}
|
||||
|
||||
public byte[] StoreBlob
|
||||
@ -191,19 +200,41 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networks)
|
||||
{
|
||||
return DefaultCrypto ?? "BTC";
|
||||
PaymentMethodId[] paymentMethodIds = GetEnabledPaymentIds(networks);
|
||||
|
||||
var defaultPaymentId = string.IsNullOrEmpty(DefaultCrypto) ? null : PaymentMethodId.Parse(DefaultCrypto);
|
||||
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ??
|
||||
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
|
||||
paymentMethodIds.FirstOrDefault();
|
||||
return chosen;
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
|
||||
public PaymentMethodId[] GetEnabledPaymentIds(BTCPayNetworkProvider networks)
|
||||
{
|
||||
DefaultCrypto = defaultCryptoCurrency;
|
||||
var excludeFilter = GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var paymentMethodIds = GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
|
||||
.Where(a => !excludeFilter.Match(a))
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC")
|
||||
.ThenBy(a => a.CryptoCode)
|
||||
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
|
||||
.ToArray();
|
||||
return paymentMethodIds;
|
||||
}
|
||||
|
||||
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
|
||||
{
|
||||
DefaultCrypto = defaultPaymentId.ToString();
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
@ -244,6 +275,12 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
@ -253,13 +290,19 @@ namespace BTCPayServer.Data
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
|
||||
[Obsolete("Use NetworkFeeMode instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool? NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool AllowCoinConversion
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public NetworkFeeMode NetworkFeeMode
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
@ -281,23 +324,9 @@ namespace BTCPayServer.Data
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule_Obsolete>();
|
||||
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
|
||||
}
|
||||
public decimal GetRateMultiplier()
|
||||
{
|
||||
decimal rate = 1.0m;
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rate;
|
||||
foreach (var rule in RateRules)
|
||||
{
|
||||
rate = rule.Apply(null, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
public decimal Spread { get; set; } = 0.0m;
|
||||
|
||||
[Obsolete]
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
@ -316,6 +345,11 @@ namespace BTCPayServer.Data
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
public CoinSwitchSettings CoinSwitchSettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
public string LightningDescriptionTemplate
|
||||
@ -336,15 +370,15 @@ namespace BTCPayServer.Data
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
@ -370,8 +404,56 @@ namespace BTCPayServer.Data
|
||||
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
rules.GlobalMultiplier = GetRateMultiplier();
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null)
|
||||
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
|
||||
else
|
||||
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
|
||||
}
|
||||
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
|
||||
return KeyPath.Parse(k);
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public EmailSettings EmailSettings { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
|
||||
return PaymentFilter.Never();
|
||||
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public bool IsExcluded(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
return GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
}
|
||||
|
||||
public void SetExcluded(PaymentMethodId paymentMethodId, bool value)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var methods = new HashSet<string>(ExcludedPaymentMethods ?? Array.Empty<string>());
|
||||
if (value)
|
||||
methods.Add(paymentMethodId.ToString());
|
||||
else
|
||||
methods.Remove(paymentMethodId.ToString());
|
||||
ExcludedPaymentMethods = methods.ToArray();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +63,18 @@ namespace BTCPayServer
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
bool hasLabel = false;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (IsLabel(parts[i]))
|
||||
{
|
||||
if (!hasLabel)
|
||||
{
|
||||
hintedLabels.Clear();
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
}
|
||||
hasLabel = true;
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
|
||||
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
|
@ -11,23 +11,14 @@ namespace BTCPayServer.Events
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
State = invoice.GetInvoiceState();
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
public string InvoiceId { get; }
|
||||
public InvoiceState State { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
return $"Invoice status is {State}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,31 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
public const string Created = "invoice_created";
|
||||
public const string ReceivedPayment = "invoice_receivedPayment";
|
||||
public const string MarkedCompleted = "invoice_markedComplete";
|
||||
public const string MarkedInvalid= "invoice_markedInvalid";
|
||||
public const string Expired= "invoice_expired";
|
||||
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
|
||||
public const string PaidInFull= "invoice_paidInFull";
|
||||
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
|
||||
public const string FailedToConfirm= "invoice_failedToConfirm";
|
||||
public const string Confirmed= "invoice_confirmed";
|
||||
public const string Completed= "invoice_completed";
|
||||
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public InvoiceEntity Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public PaymentEntity Payment { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
|
@ -32,6 +32,7 @@ using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -60,6 +61,23 @@ namespace BTCPayServer
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
|
||||
{
|
||||
if (value != 0m)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
@ -107,6 +125,12 @@ namespace BTCPayServer
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
public static string WithStartingSlash(this string str)
|
||||
{
|
||||
if (str.StartsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return $"/{str}";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
@ -128,6 +152,19 @@ namespace BTCPayServer
|
||||
resp.Headers[name] = value;
|
||||
}
|
||||
|
||||
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
if (IsSegwitCore(derivationStrategyBase))
|
||||
return true;
|
||||
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
|
||||
}
|
||||
|
||||
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
|
||||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -137,6 +174,58 @@ namespace BTCPayServer
|
||||
request.PathBase.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrl(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
||||
/// If 'toto' and RootPath is empty returns '/toto'
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePath(this HttpRequest request, string path)
|
||||
{
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'https://example.com/toto' returns 'https://example.com/toto'
|
||||
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
||||
/// If 'toto' and RootPath is empty returns '/toto'
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
|
||||
return path;
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
path);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
@ -145,6 +234,31 @@ namespace BTCPayServer
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will return an absolute URL.
|
||||
/// If `relativeOrAsbolute` is absolute, returns it.
|
||||
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="relativeOrAbsolte"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
|
||||
{
|
||||
if (relativeOrAbsolute == null)
|
||||
{
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()), UriKind.Absolute);
|
||||
}
|
||||
if (relativeOrAbsolute.IsAbsoluteUri)
|
||||
return relativeOrAbsolute;
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -232,5 +346,15 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static string TrimEnd(this string input, string suffixToRemove,
|
||||
StringComparison comparisonType) {
|
||||
|
||||
if (input != null && suffixToRemove != null
|
||||
&& input.EndsWith(suffixToRemove, comparisonType)) {
|
||||
return input.Substring(0, input.Length - suffixToRemove.Length);
|
||||
}
|
||||
else return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
public static class EmailSenderExtensions
|
||||
{
|
||||
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
|
||||
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
|
||||
{
|
||||
return emailSender.SendEmailAsync(email, "Confirm your email",
|
||||
emailSender.SendEmail(email, "Confirm your email",
|
||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,28 @@ namespace BTCPayServer.Filters
|
||||
}
|
||||
}
|
||||
|
||||
public class MediaTypeAcceptConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public MediaTypeAcceptConstraintAttribute(string mediaType)
|
||||
{
|
||||
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey("Accept"))
|
||||
return false;
|
||||
return context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().StartsWith(MediaType, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
|
||||
|
@ -12,18 +12,48 @@ namespace BTCPayServer.Filters
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value
|
||||
|
||||
public XFrameOptionsAttribute(XFrameOptions type, string allowFrom = null)
|
||||
{
|
||||
get; set;
|
||||
switch (type)
|
||||
{
|
||||
case XFrameOptions.Deny:
|
||||
Value = "deny";
|
||||
break;
|
||||
case XFrameOptions.SameOrigin:
|
||||
Value = "deny";
|
||||
break;
|
||||
case XFrameOptions.AllowFrom:
|
||||
Value = $"allow-from {allowFrom}";
|
||||
break;
|
||||
case XFrameOptions.AllowAll:
|
||||
Value = "allow-all";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
}
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
if (context.IsEffectivePolicy<XFrameOptionsAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
|
||||
public enum XFrameOptions
|
||||
{
|
||||
Deny,
|
||||
SameOrigin,
|
||||
AllowFrom,
|
||||
AllowAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class BackgroundJobSchedulerHostedService : IHostedService
|
||||
{
|
||||
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
|
||||
{
|
||||
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
|
||||
}
|
||||
|
||||
public BackgroundJobClient BackgroundJobClient { get; }
|
||||
|
||||
Task _Loop;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Stop = new CancellationTokenSource();
|
||||
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
CancellationTokenSource _Stop;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Stop.Cancel();
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackgroundJobClient : IBackgroundJobClient
|
||||
{
|
||||
class BackgroundJob
|
||||
{
|
||||
public Func<Task> Action;
|
||||
public TimeSpan Delay;
|
||||
public IDelay DelayImplementation;
|
||||
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
{
|
||||
this.Action = action;
|
||||
this.Delay = delay;
|
||||
this.DelayImplementation = delayImplementation;
|
||||
}
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
await DelayImplementation.Wait(Delay, cancellationToken);
|
||||
await Action();
|
||||
}
|
||||
}
|
||||
|
||||
public IDelay Delay { get; set; } = TaskDelay.Instance;
|
||||
public int GetExecutingCount()
|
||||
{
|
||||
lock (_Processing)
|
||||
{
|
||||
return _Processing.Count();
|
||||
}
|
||||
}
|
||||
|
||||
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
|
||||
HashSet<Task> _Processing = new HashSet<Task>();
|
||||
public void Schedule(Func<Task> action, TimeSpan delay)
|
||||
{
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
|
||||
}
|
||||
|
||||
public async Task WaitAllRunning(CancellationToken cancellationToken)
|
||||
{
|
||||
Task[] processing = null;
|
||||
lock (_Processing)
|
||||
{
|
||||
processing = _Processing.ToArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(processing).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessJobs(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
if (_Jobs.Reader.TryRead(out var job))
|
||||
{
|
||||
var processing = job.Run(cancellationToken);
|
||||
lock (_Processing)
|
||||
{
|
||||
_Processing.Add(processing);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
processing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
|
||||
}
|
||||
lock (_Processing)
|
||||
{
|
||||
_Processing.Remove(processing);
|
||||
}
|
||||
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CheckConfigurationHostedService : IHostedService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
|
||||
public CheckConfigurationHostedService(BTCPayServerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
if (_options.SSHSettings != null)
|
||||
{
|
||||
Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ...");
|
||||
var connection = new Renci.SshNet.SshClient(_options.SSHSettings.CreateConnectionInfo());
|
||||
connection.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
|
||||
{
|
||||
e.CanTrust = true;
|
||||
if (!_options.IsTrustedFingerprint(e.FingerPrint, e.HostKey))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
connection.Connect();
|
||||
connection.Disconnect();
|
||||
Logs.Configuration.LogInformation($"SSH connection succeeded");
|
||||
}
|
||||
catch (Renci.SshNet.Common.SshAuthenticationException)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH invalid credentials");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
|
||||
{
|
||||
message = aggrEx.InnerException.Message;
|
||||
}
|
||||
Logs.Configuration.LogWarning($"SSH connection issue: {message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
})
|
||||
{ IsBackground = true }.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
85
BTCPayServer/HostedServices/EventHostedServiceBase.cs
Normal file
85
BTCPayServer/HostedServices/EventHostedServiceBase.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class EventHostedServiceBase : IHostedService
|
||||
{
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
|
||||
private List<IEventAggregatorSubscription> _Subscriptions;
|
||||
private CancellationTokenSource _Cts;
|
||||
|
||||
public EventHostedServiceBase(EventAggregator eventAggregator)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
Channel<object> _Events = Channel.CreateUnbounded<object>();
|
||||
public async Task ProcessEvents(CancellationToken cancellationToken)
|
||||
{
|
||||
while (await _Events.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
if (_Events.Reader.TryRead(out var evt))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, $"Unhandled exception in {this.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
protected virtual void SubscibeToEvents()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected void Subscribe<T>()
|
||||
{
|
||||
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
|
||||
}
|
||||
|
||||
public virtual Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions = new List<IEventAggregatorSubscription>();
|
||||
SubscibeToEvents();
|
||||
_Cts = new CancellationTokenSource();
|
||||
_ProcessingEvents = ProcessEvents(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
Task _ProcessingEvents = Task.CompletedTask;
|
||||
|
||||
public virtual async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
||||
_Cts?.Cancel();
|
||||
try
|
||||
{
|
||||
await _ProcessingEvents;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire.Annotations;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@ -20,6 +17,8 @@ using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -34,108 +33,132 @@ namespace BTCPayServer.HostedServices
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceEntity Invoice
|
||||
public InvoicePaymentNotificationEventWrapper Notification
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IBackgroundJobClient _JobClient;
|
||||
EventAggregator _EventAggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
EmailSenderFactory emailSenderFactory)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
|
||||
{
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var notification = new InvoicePaymentNotificationEventWrapper()
|
||||
{
|
||||
Data = new InvoicePaymentNotification()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Currency = dto.Currency,
|
||||
CurrentTime = dto.CurrentTime,
|
||||
ExceptionStatus = dto.ExceptionStatus,
|
||||
ExpirationTime = dto.ExpirationTime,
|
||||
InvoiceTime = dto.InvoiceTime,
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
|
||||
PaymentSubtotals = dto.PaymentSubtotals,
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates,
|
||||
},
|
||||
Event = new InvoicePaymentNotificationEvent()
|
||||
{
|
||||
Code = invoiceEvent.EventCode,
|
||||
Name = invoiceEvent.Name
|
||||
},
|
||||
ExtendedNotification = extendedNotification,
|
||||
NotificationURL = invoice.NotificationURL
|
||||
};
|
||||
|
||||
// For lightning network payments, paid, confirmed and completed come all at once.
|
||||
// So despite the event is "paid" or "confirmed" the Status of the invoice is technically complete
|
||||
// This confuse loggers who think their endpoint get duplicated events
|
||||
// So here, we just override the status expressed by the notification
|
||||
if (invoiceEvent.Name == InvoiceEvent.Confirmed)
|
||||
{
|
||||
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Confirmed);
|
||||
}
|
||||
if (invoiceEvent.Name == InvoiceEvent.PaidInFull)
|
||||
{
|
||||
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Paid);
|
||||
}
|
||||
//////////////////
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Data.Rate = dto.Rate;
|
||||
notification.Data.Url = dto.Url;
|
||||
notification.Data.BTCDue = dto.BTCDue;
|
||||
notification.Data.BTCPaid = dto.BTCPaid;
|
||||
notification.Data.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
|
||||
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
|
||||
{
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
var response = await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(notification);
|
||||
|
||||
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
|
||||
invoice.NotificationEmail,
|
||||
$"BtcPayServer Invoice Notification - ${invoice.StoreId}",
|
||||
emailBody);
|
||||
|
||||
}
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL) || !Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute))
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
var jobId = GetHttpJobId(job.Invoice);
|
||||
|
||||
if (!_Executing.TryAdd(jobId, jobId))
|
||||
return; //For some reason, Hangfire fire the job several time
|
||||
|
||||
Logger.LogInformation("Running " + jobId);
|
||||
bool reschedule = false;
|
||||
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
|
||||
reschedule = !response.IsSuccessStatusCode;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
});
|
||||
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
aggregatorEvent.Error = "Timeout";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " timed out");
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
catch (Exception ex)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
reschedule = true;
|
||||
|
||||
List<string> messages = new List<string>();
|
||||
@ -145,21 +168,16 @@ namespace BTCPayServer.HostedServices
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
aggregatorEvent.Error = $"Unexpected error: {message}";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
finally { cts?.Dispose(); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if (job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
|
||||
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
@ -178,63 +196,35 @@ namespace BTCPayServer.HostedServices
|
||||
public InvoicePaymentNotificationEvent Event { get; set; }
|
||||
[JsonProperty("data")]
|
||||
public InvoicePaymentNotification Data { get; set; }
|
||||
[JsonProperty("extendedNotification")]
|
||||
public bool ExtendedNotification { get; set; }
|
||||
[JsonProperty(PropertyName = "notificationURL")]
|
||||
public string NotificationURL { get; set; }
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
InvoicePaymentNotification notification = new InvoicePaymentNotification()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Currency = dto.Currency,
|
||||
CurrentTime = dto.CurrentTime,
|
||||
ExceptionStatus = dto.ExceptionStatus,
|
||||
ExpirationTime = dto.ExpirationTime,
|
||||
InvoiceTime = dto.InvoiceTime,
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
|
||||
PaymentSubtotals = dto.PaymentSubtotals,
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates
|
||||
};
|
||||
var notificationString = NBitcoin.JsonConverters.Serializer.ToString(notification);
|
||||
var jobj = JObject.Parse(notificationString);
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
|
||||
if (btcCryptoInfo != null)
|
||||
if (notification.ExtendedNotification)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = dto.Rate;
|
||||
notification.Url = dto.Url;
|
||||
notification.BTCDue = dto.BTCDue;
|
||||
notification.BTCPaid = dto.BTCPaid;
|
||||
notification.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
string notificationString = null;
|
||||
if (eventCode.HasValue)
|
||||
{
|
||||
var wrapper = new InvoicePaymentNotificationEventWrapper();
|
||||
wrapper.Data = notification;
|
||||
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
|
||||
notificationString = JsonConvert.SerializeObject(wrapper);
|
||||
jobj.Remove("extendedNotification");
|
||||
jobj.Remove("notificationURL");
|
||||
notificationString = jobj.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationString = JsonConvert.SerializeObject(notification);
|
||||
notificationString = jobj["data"].ToString();
|
||||
}
|
||||
|
||||
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
|
||||
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -263,15 +253,15 @@ namespace BTCPayServer.HostedServices
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
if (t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
if (t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
@ -288,7 +278,7 @@ namespace BTCPayServer.HostedServices
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
if (executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
@ -298,17 +288,12 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
{
|
||||
return $"{invoice.Id}-{invoice.Status}-HTTP";
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id);
|
||||
if (invoice == null)
|
||||
return;
|
||||
List<Task> tasks = new List<Task>();
|
||||
@ -319,27 +304,27 @@ namespace BTCPayServer.HostedServices
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed" ||
|
||||
e.Name == "invoice_expiredPaidPartial"
|
||||
if (e.Name == InvoiceEvent.Expired ||
|
||||
e.Name == InvoiceEvent.PaidInFull ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
e.Name == InvoiceEvent.MarkedInvalid ||
|
||||
e.Name == InvoiceEvent.MarkedCompleted ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
e.Name == InvoiceEvent.Completed ||
|
||||
e.Name == InvoiceEvent.ExpiredPaidPartial
|
||||
)
|
||||
tasks.Add(Notify(invoice));
|
||||
Notify(invoice, e, false);
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
if (e.Name == InvoiceEvent.Confirmed)
|
||||
{
|
||||
tasks.Add(Notify(invoice));
|
||||
Notify(invoice, e, false);
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||
Notify(invoice, e, true);
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -11,7 +11,6 @@ using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
@ -61,15 +60,15 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
if(invoice.ExceptionStatus == "paidPartial")
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
|
||||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@ -78,57 +77,57 @@ namespace BTCPayServer.HostedServices
|
||||
if (paymentMethod == null)
|
||||
return;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired)
|
||||
{
|
||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
if (invoice.Status == InvoiceStatus.Paid)
|
||||
{
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
invoice.Status = InvoiceStatus.New;
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
if (invoice.Status == InvoiceStatus.Paid)
|
||||
{
|
||||
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
|
||||
|
||||
@ -139,26 +138,26 @@ namespace BTCPayServer.HostedServices
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
if (invoice.Status == InvoiceStatus.Confirmed)
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@ -208,7 +207,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
try
|
||||
{
|
||||
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
@ -247,13 +246,13 @@ namespace BTCPayServer.HostedServices
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
@ -283,14 +282,14 @@ namespace BTCPayServer.HostedServices
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
@ -299,8 +298,8 @@ namespace BTCPayServer.HostedServices
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
|
@ -53,6 +53,24 @@ namespace BTCPayServer.HostedServices
|
||||
settings.UnreachableStoreCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertMultiplierToSpread)
|
||||
{
|
||||
await ConvertMultiplierToSpread();
|
||||
settings.ConvertMultiplierToSpread = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertNetworkFeeProperty)
|
||||
{
|
||||
await ConvertNetworkFeeProperty();
|
||||
settings.ConvertNetworkFeeProperty = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertCrowdfundOldSettings)
|
||||
{
|
||||
await ConvertCrowdfundOldSettings();
|
||||
settings.ConvertCrowdfundOldSettings = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -61,6 +79,69 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertCrowdfundOldSettings()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var app in ctx.Apps.Where(a => a.AppType == "Crowdfund"))
|
||||
{
|
||||
var settings = app.GetSettings<Services.Apps.CrowdfundSettings>();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (settings.UseAllStoreInvoices)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
app.TagAllInvoices = true;
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertNetworkFeeProperty()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
var blob = store.GetStoreBlob();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (blob.NetworkFeeDisabled != null)
|
||||
{
|
||||
blob.NetworkFeeMode = blob.NetworkFeeDisabled.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always;
|
||||
blob.NetworkFeeDisabled = null;
|
||||
store.SetStoreBlob(blob);
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertMultiplierToSpread()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
var blob = store.GetStoreBlob();
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
decimal multiplier = 1.0m;
|
||||
if (blob.RateRules != null && blob.RateRules.Count != 0)
|
||||
{
|
||||
foreach (var rule in blob.RateRules)
|
||||
{
|
||||
multiplier = rule.Apply(null, multiplier);
|
||||
}
|
||||
}
|
||||
blob.RateRules = null;
|
||||
blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m)));
|
||||
store.SetStoreBlob(blob);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnreachableStoreCheck()
|
||||
{
|
||||
return _StoreRepository.CleanUnreachableStores();
|
||||
|
@ -43,11 +43,15 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
||||
public NBXplorerSummary Get(string cryptoCode)
|
||||
{
|
||||
_Summaries.TryGetValue(cryptoCode, out var summary);
|
||||
return summary;
|
||||
}
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
|
@ -18,9 +18,9 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
BTCPayRateProviderFactory _RateProviderFactory;
|
||||
RateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
RateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
@ -33,16 +33,40 @@ namespace BTCPayServer.HostedServices
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings)
|
||||
CreateLoopTask(RefreshCoinAverageSettings),
|
||||
CreateLoopTask(RefreshRates)
|
||||
};
|
||||
}
|
||||
async Task RefreshRates()
|
||||
{
|
||||
|
||||
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
|
||||
{
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_RateProviderFactory.Providers
|
||||
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
|
||||
}
|
||||
}, TaskScheduler.Default))
|
||||
.ToArray()).WithCancellation(timeout.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach(var item in tickers
|
||||
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
{
|
||||
@ -54,7 +78,6 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
|
@ -38,7 +38,16 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Npgsql;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -51,6 +60,7 @@ namespace BTCPayServer.Hosting
|
||||
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
|
||||
factory.ConfigureBuilder(o);
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
@ -66,25 +76,31 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
ApplicationDbContextFactory dbContext = null;
|
||||
if (opts.PostgresConnectionString == null)
|
||||
if (!String.IsNullOrEmpty(opts.PostgresConnectionString))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
else if(!String.IsNullOrEmpty(opts.MySQLConnectionString))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"MySQL DB used ({opts.MySQLConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
return dbContext;
|
||||
});
|
||||
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
|
||||
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
@ -92,33 +108,88 @@ namespace BTCPayServer.Hosting
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
{
|
||||
|
||||
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
|
||||
|
||||
|
||||
htmlSanitizer.RemovingAtRule += (sender, args) =>
|
||||
{
|
||||
};
|
||||
htmlSanitizer.RemovingTag += (sender, args) =>
|
||||
{
|
||||
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 == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
};
|
||||
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");
|
||||
return htmlSanitizer;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<LightningConfigurationProvider>();
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<PaymentRequestRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
Fallback = new FeeRate(100L, 1),
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
|
||||
services.Configure<MvcOptions>((o) => {
|
||||
o.Filters.Add(new ContentSecurityPolicyCssThemeManager());
|
||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
|
||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
|
||||
});
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
@ -129,13 +200,16 @@ namespace BTCPayServer.Hosting
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
services.TryAddSingleton<BTCPayRateProviderFactory>();
|
||||
services.TryAddSingleton<RateProviderFactory>();
|
||||
services.TryAddSingleton<RateFetcher>();
|
||||
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
services.AddTransient<AppsPublicController>();
|
||||
services.AddTransient<PaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
@ -146,11 +220,19 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
var bundle = new BundleOptions();
|
||||
bundle.UseMinifiedFiles = opts.BundleJsCss;
|
||||
bundle.UseBundles = opts.BundleJsCss;
|
||||
bundle.AppendVersion = true;
|
||||
return bundle;
|
||||
});
|
||||
|
||||
services.AddCors(options=>
|
||||
{
|
||||
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var rateLimits = new RateLimitService();
|
||||
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
|
||||
services.AddSingleton(rateLimits);
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -171,7 +253,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
static void Retry(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(1000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@ -179,7 +261,9 @@ namespace BTCPayServer.Hosting
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch when(!cts.IsCancellationRequested)
|
||||
// Starting up
|
||||
catch (PostgresException ex) when (ex.SqlState == "57P03") { Thread.Sleep(1000); }
|
||||
catch when (!cts.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
@ -38,9 +38,20 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
|
||||
if (isBitpayAPI && httpContext.Request.Method == "OPTIONS")
|
||||
{
|
||||
httpContext.Response.StatusCode = 200;
|
||||
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
|
||||
if (httpContext.Request.Headers.ContainsKey("Access-Control-Request-Headers"))
|
||||
{
|
||||
httpContext.Response.SetHeader("Access-Control-Allow-Headers", httpContext.Request.Headers["Access-Control-Request-Headers"].FirstOrDefault());
|
||||
}
|
||||
return; // We bypass MVC completely
|
||||
}
|
||||
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||
if (isBitpayAPI)
|
||||
{
|
||||
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
|
||||
httpContext.SetBitpayAuth(bitpayAuth);
|
||||
}
|
||||
await _Next(httpContext);
|
||||
@ -81,32 +92,34 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
var method = httpContext.Request.Method;
|
||||
var isCors = method == "OPTIONS";
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
(isCors || bitpayAuth) &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(isCors || (method == "POST" && isJson)))
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
(isCors || bitpayAuth) &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(isCors || method == "GET"))
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
(isCors || method == "GET") &&
|
||||
(isCors || isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
(isCors || method == "GET"))
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(isCors || method == "GET" || method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@ -134,63 +147,33 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
|
@ -19,7 +19,6 @@ using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Threading.Tasks;
|
||||
@ -27,55 +26,45 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Hangfire.AspNetCore;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.IO;
|
||||
using Hangfire.Dashboard;
|
||||
using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
class NeedRole : IDashboardAuthorizationFilter
|
||||
{
|
||||
string _Role;
|
||||
public NeedRole(string role)
|
||||
{
|
||||
_Role = role;
|
||||
}
|
||||
public bool Authorize([NotNull] DashboardContext context)
|
||||
{
|
||||
return context.GetHttpContext().User.IsInRole(_Role);
|
||||
}
|
||||
}
|
||||
public Startup(IConfiguration conf, IHostingEnvironment env)
|
||||
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
|
||||
{
|
||||
Configuration = conf;
|
||||
_Env = env;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
IHostingEnvironment _Env;
|
||||
public IConfiguration Configuration
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
Logs.Configure(LoggerFactory);
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
services.AddMemoryCache();
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.AddSignalR();
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
@ -96,34 +85,50 @@ namespace BTCPayServer.Hosting
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequiredLength = 7;
|
||||
options.Password.RequiredLength = 6;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
});
|
||||
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
|
||||
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
|
||||
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
|
||||
bool hasCertPath = !String.IsNullOrEmpty(httpsCertificateFilePath);
|
||||
if (hasCertPath || useDefaultCertificate)
|
||||
{
|
||||
var bindAddress = Configuration.GetOrDefault<IPAddress>("bind", IPAddress.Any);
|
||||
int bindPort = Configuration.GetOrDefault<int>("port", 443);
|
||||
|
||||
services.AddHangfire((o) =>
|
||||
{
|
||||
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
|
||||
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
|
||||
options.ConfigureHangfireBuilder(o);
|
||||
});
|
||||
services.AddCors(o =>
|
||||
{
|
||||
o.AddPolicy("BitpayAPI", b =>
|
||||
services.Configure<KestrelServerOptions>(kestrel =>
|
||||
{
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
if (hasCertPath && !File.Exists(httpsCertificateFilePath))
|
||||
{
|
||||
// Note that by design this is a fatal error condition that will cause the process to exit.
|
||||
throw new ConfigException($"The https certificate file could not be found at {httpsCertificateFilePath}.");
|
||||
}
|
||||
if(hasCertPath && useDefaultCertificate)
|
||||
{
|
||||
throw new ConfigException($"Conflicting settings: if HttpsUseDefaultCertificate is true, HttpsCertificateFilePath should not be used");
|
||||
}
|
||||
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
//{
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// {
|
||||
// l.UseHttps("devtest.pfx", "toto");
|
||||
// });
|
||||
//});
|
||||
kestrel.Listen(bindAddress, bindPort, l =>
|
||||
{
|
||||
if (hasCertPath)
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Using HTTPS with the certificate located in {httpsCertificateFilePath}.");
|
||||
l.UseHttps(httpsCertificateFilePath, Configuration.GetOrDefault<string>("HttpsCertificateFilePassword", null));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Using HTTPS with the default certificate");
|
||||
l.UseHttps();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
@ -133,7 +138,6 @@ namespace BTCPayServer.Hosting
|
||||
BTCPayServerOptions options,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
Logs.Configure(loggerFactory);
|
||||
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
|
||||
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -155,16 +159,17 @@ namespace BTCPayServer.Hosting
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
AppPath = options.GetRootUri(),
|
||||
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(
|
||||
|
33
BTCPayServer/IDelay.cs
Normal file
33
BTCPayServer/IDelay.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public interface IDelay
|
||||
{
|
||||
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class TaskDelay : IDelay
|
||||
{
|
||||
TaskDelay()
|
||||
{
|
||||
|
||||
}
|
||||
private static readonly TaskDelay _Instance = new TaskDelay();
|
||||
public static TaskDelay Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class LightMoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
Type longType = typeof(long).GetTypeInfo();
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
reader.TokenType == JsonToken.Integer ?
|
||||
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
|
||||
: new LightMoney(long.MaxValue) :
|
||||
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||
: null;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user