Compare commits
1000 Commits
v1.0.1.16
...
v1.0.2.106
Author | SHA1 | Date | |
---|---|---|---|
b750663a1f | |||
4c4b76e995 | |||
da19d2c1a7 | |||
fb15c5b354 | |||
6ffe1cfcab | |||
87678c58ac | |||
feab4cc48a | |||
712946f512 | |||
a7bfceae05 | |||
8a26cd549a | |||
1cf3ce0617 | |||
73c65fada2 | |||
92ea923c03 | |||
e7db453717 | |||
10ee09f052 | |||
be7d91a138 | |||
3278c80d3f | |||
65e1edb0b8 | |||
e05c88370f | |||
15c29f8419 | |||
fc722731d3 | |||
1c9c564e90 | |||
872b60f8ea | |||
0d3364b3da | |||
fed53661b3 | |||
e86b4d89ca | |||
c5cb32f6dd | |||
deb56e16ec | |||
b5626ef01c | |||
e39d9067f2 | |||
43d34d5d35 | |||
7341be76bb | |||
0abd62dfe8 | |||
735012e3d7 | |||
b5efb8d2e6 | |||
1dbeabb716 | |||
671f9e56e2 | |||
dc6c189948 | |||
4501824f3f | |||
4568d2a98e | |||
f5d81334f8 | |||
f3ed90399b | |||
fada01cec9 | |||
1b4b9fb4cc | |||
6eeef8a866 | |||
24979a0af2 | |||
be1a44f018 | |||
9fcc2903fc | |||
06df63b283 | |||
0f1efc16f5 | |||
da8a06952c | |||
38d810cef7 | |||
393a3a2b8f | |||
aaddc580d1 | |||
957d478865 | |||
023913a852 | |||
6c51d83f61 | |||
0edaedb6ab | |||
13f21aa0d6 | |||
929a0c37bd | |||
058ccf56d0 | |||
162ac572da | |||
c7f3fdb46d | |||
29af07b3f9 | |||
758436a428 | |||
e0cadb4f62 | |||
013dfa1b61 | |||
e0f1c50534 | |||
d50dc2e68e | |||
8b5b18c97e | |||
f7383b4cc8 | |||
1bc32285ba | |||
f12114f9aa | |||
2b6faa8d20 | |||
45b7df6ac9 | |||
5a43ce2719 | |||
fe31dc8606 | |||
f0615482d9 | |||
03c47e6f7d | |||
4111b8a5a3 | |||
5b5a2e8c25 | |||
9ec0c23c52 | |||
9a5034c13c | |||
b1fcf4524a | |||
87d384dba5 | |||
4f5a8f7953 | |||
8728356698 | |||
9c30476fc8 | |||
09beb57eaf | |||
76a36d1829 | |||
0d4036efa2 | |||
cb4562aad5 | |||
0084d4766b | |||
74ddcfa01e | |||
ed36fba0d7 | |||
af015d435b | |||
ec59980e6f | |||
f0f4247c5d | |||
b562094956 | |||
4afd55c441 | |||
d7404f418d | |||
893410911c | |||
556b581b6a | |||
1685ccaca8 | |||
522abcfdfd | |||
ed1827ff28 | |||
214b2d1c1c | |||
322518e9dc | |||
ea2dd536b4 | |||
6a1eca760a | |||
29513d4ded | |||
57daf27fdd | |||
e698d90e3c | |||
86ca081030 | |||
14841ad7c9 | |||
2c6aa12aab | |||
ef4d39db3c | |||
7a566c477d | |||
85c40aef23 | |||
d00fa42553 | |||
e9e94f5e99 | |||
5e2d4407ca | |||
846bd08e20 | |||
a1a4eed860 | |||
1e582625f3 | |||
39b018fdf3 | |||
83304de1c6 | |||
5c8e03dcbf | |||
4dddc539f6 | |||
9950b781b4 | |||
7a32f692d1 | |||
d480be925b | |||
3775317047 | |||
500bdd9bf1 | |||
57bda24664 | |||
6401af00fe | |||
3b3a18bbbc | |||
b4e9dfeeaf | |||
16f5def245 | |||
26e7de534b | |||
a3ae694048 | |||
b04d70f141 | |||
101d6131c7 | |||
faabd68f6f | |||
aa72b814da | |||
0dcda0f289 | |||
a2b039f983 | |||
1a54f2d01a | |||
4276994265 | |||
2c6133b4f7 | |||
64181d1a93 | |||
25e9a27a78 | |||
f3edaf5160 | |||
7bfdf2d11d | |||
d2808cf662 | |||
b68fcec692 | |||
86644d38d7 | |||
52f60b0457 | |||
638b58ab48 | |||
1606f43609 | |||
ad1307746c | |||
22307b8a2b | |||
f1c244bf6a | |||
c425e0439d | |||
cdf78f0463 | |||
7d997f7d60 | |||
0edbbe6ae3 | |||
4f7bfbf451 | |||
cd416dac60 | |||
b58173967d | |||
4b743bb998 | |||
0af1adcfb8 | |||
c048e4eeaf | |||
64194896ba | |||
1d4472cc08 | |||
d8bc7ef4b8 | |||
27cea81cb2 | |||
b0d6216ffc | |||
060876d07f | |||
96b7373d88 | |||
c2f282f85c | |||
7afa726ddc | |||
6eb7bbf853 | |||
11a26c940d | |||
624252e4ad | |||
57bb980526 | |||
e0c718f4ba | |||
c58e015bfb | |||
648829644a | |||
1b0f8c7aca | |||
4f8e0b0393 | |||
466f65d6cd | |||
022b4f115d | |||
71f6aaabbd | |||
79b06bce42 | |||
480afebcd9 | |||
96721e95a2 | |||
883cd41232 | |||
3f48a478af | |||
8d3b45bdec | |||
bbd19a96ec | |||
ce17e3212a | |||
c3ea63c6ce | |||
1ee4bd9c92 | |||
e6bb6619e5 | |||
cc29d863d7 | |||
8cb2c93abd | |||
2187e05a10 | |||
4c07483383 | |||
65916755b6 | |||
3cefbc89e4 | |||
c40b47b1dd | |||
a2d17bfa7e | |||
cdf0c6d27d | |||
8154986102 | |||
203494e809 | |||
d49bbc95af | |||
c44132fd35 | |||
c75512303d | |||
97d50df13e | |||
0e1ef78af1 | |||
464ab30fea | |||
3ee1c05646 | |||
c54c39926b | |||
33d18a3278 | |||
97e564901e | |||
832069dd44 | |||
1ac17e96c3 | |||
d907031ec7 | |||
4b5af9cb5c | |||
0057146fee | |||
0c8925d2a2 | |||
b9e5b0d56e | |||
eb6dbd1247 | |||
fe8428b8b0 | |||
f2aa15310a | |||
1814cb2d6e | |||
94a6f20a05 | |||
22e700a53e | |||
cd78e559cf | |||
f0257fb8f7 | |||
34cdbf73f0 | |||
b291a6d25a | |||
fa7e974e73 | |||
976d9d0cda | |||
6ea2d9175d | |||
10ceddc709 | |||
5dd57c8064 | |||
a256dd3277 | |||
5ee9a92f1e | |||
65c7c85c14 | |||
27b686095c | |||
cd2fef0dab | |||
743288fa47 | |||
270ebead49 | |||
145e3bec83 | |||
563e931468 | |||
3113097c4f | |||
cdbbad1694 | |||
c9c2730409 | |||
310a9a6d59 | |||
1a1078782e | |||
73cb3dc4ee | |||
9eb36a8b40 | |||
6307aa8665 | |||
b9e8408db5 | |||
0879895678 | |||
a4ecf070b0 | |||
162d76206e | |||
5af14ef2ec | |||
7210eebeca | |||
25dbf6445f | |||
0828c60537 | |||
34deb17f3d | |||
06b02b8691 | |||
b7abc08c27 | |||
399ae2cd9e | |||
63fe0f6612 | |||
42d60ef84b | |||
1784c30787 | |||
ac8feceaf2 | |||
3d8c5195ae | |||
9a5259510b | |||
caecb26420 | |||
ecc8b3d9ed | |||
d313395751 | |||
9e698a8004 | |||
3c4c99ee42 | |||
d34ffc0d9a | |||
039303bfaa | |||
273cf1adc9 | |||
5feb520843 | |||
17e914778d | |||
db24ab792f | |||
42475ec7b7 | |||
4972f0ab7b | |||
07e13747cf | |||
2465eb7e36 | |||
4ddcd7a4c8 | |||
89d9658e82 | |||
66ecb32538 | |||
a22576da0a | |||
69bd888bab | |||
9b540273fc | |||
cfd083bed5 | |||
55c9314cdd | |||
8cafa8a483 | |||
448cc06a11 | |||
0780df4fd7 | |||
04174b7431 | |||
b7c58c2083 | |||
cd75fd6842 | |||
370951a3bd | |||
2c08b0137b | |||
724af44e41 | |||
1eee31e9f1 | |||
01cf579530 | |||
f72705935a | |||
a29ab6b6b0 | |||
4784518235 | |||
f8c88bd44f | |||
0d1d0d57f4 | |||
2bd1238668 | |||
d1fb51b412 | |||
279de1b869 | |||
ce9189caf8 | |||
431147784e | |||
0697b8bf86 | |||
5050b59014 | |||
665cf4c3b1 | |||
bac9ef4f93 | |||
98e81ab0fd | |||
6ce70237fc | |||
4f23fc99a1 | |||
d7fccae452 | |||
d7a5021ed2 | |||
63ec832667 | |||
8d95b9fa04 | |||
ada6f3b844 | |||
c8a26ce952 | |||
6cf80b7533 | |||
79df523bb2 | |||
e921f9757a | |||
b497d1871e | |||
c7cd029482 | |||
68f2cba60d | |||
5c4200b036 | |||
bc06114023 | |||
bed9737d64 | |||
556082c4c9 | |||
6a46d02fc6 | |||
d75e5b8b12 | |||
d293bc3947 | |||
e634700913 | |||
ce81136c88 | |||
a97ef2eee8 | |||
be33ebc168 | |||
789193a0c8 | |||
01792cf299 | |||
ff9265f721 | |||
8d61314852 | |||
1ce6ae8727 | |||
dec5dbc0d2 | |||
4e32dad1ea | |||
127ca7582f | |||
b98993f84b | |||
e35f074b66 | |||
ba3d13d56c | |||
ead67887ab | |||
437f27f107 | |||
8d41a8e98d | |||
7e6ab015a6 | |||
2583eb15ec | |||
1879ea55e8 | |||
f8bc3a5081 | |||
dd1a93ee0e | |||
093ae39e61 | |||
cac58808f0 | |||
a063f10778 | |||
3cf3aa63f6 | |||
011dd5574f | |||
365911286b | |||
fe5347aa86 | |||
f22c8a72cd | |||
eeb522fe7d | |||
f9e40b209a | |||
20635ea3d6 | |||
6cefd9c3e7 | |||
7062705d6f | |||
58b994e043 | |||
640ff36fa2 | |||
39ec5242d7 | |||
1c50210e61 | |||
a1ffda0151 | |||
fd15348551 | |||
989c99c550 | |||
bcf97b1474 | |||
67abbed66a | |||
eb01e91e13 | |||
12ceb9e0bc | |||
ecf03f90aa | |||
1747414a57 | |||
3a02f16c6e | |||
a6ee337ed0 | |||
559f535257 | |||
2952ccc7fd | |||
a0243fa569 | |||
789b9168ad | |||
7c29cb62ef | |||
f81ca1888d | |||
ed02e0f4d6 | |||
0a83f21af5 | |||
23a3c145ed | |||
4184c6c208 | |||
29c28b1841 | |||
de48fb4077 | |||
bcd79c5882 | |||
b8c513aa2b | |||
ad67f4ef18 | |||
2c0bcfc0ec | |||
0ba1072d54 | |||
f7fe855274 | |||
449738414b | |||
a34842585d | |||
eb882c2c46 | |||
ca65c6bd8f | |||
f97173e9e7 | |||
8fc1b0c856 | |||
cabd7c4e64 | |||
f8540dc78c | |||
b03d271f85 | |||
3770adb7d3 | |||
7fdf19ca22 | |||
4e776adb03 | |||
26db946392 | |||
d102c142b9 | |||
f7989541b9 | |||
b7f0ce18b3 | |||
e1dfbfe3b0 | |||
786d129452 | |||
a37a8e8fcd | |||
355989c278 | |||
af3dee95de | |||
70a6bd6a01 | |||
4afb0acc84 | |||
9afc143801 | |||
8e4943df65 | |||
9b3bd8343d | |||
ee4f83ddba | |||
c326998381 | |||
239a011e60 | |||
5ffe118159 | |||
6f07849e1d | |||
dbe5c62d11 | |||
199db01eaf | |||
a3c46c8f67 | |||
66a68d6180 | |||
be1128a886 | |||
d41a5a65a2 | |||
d5cab938ee | |||
9dddfb65f0 | |||
6bd5976d90 | |||
b3385bf901 | |||
bba268b5e2 | |||
70c98b6901 | |||
2d3b7fea2e | |||
3bdf1c9a00 | |||
a52665ea80 | |||
3d943d49e6 | |||
6ca8ba9231 | |||
75d685ae6c | |||
7b2ef9aec2 | |||
efe666b284 | |||
ca8af5047a | |||
cdc0b0d628 | |||
87e28b70fd | |||
b96f464e39 | |||
bca68986f3 | |||
272ac49872 | |||
5f05ca5ac6 | |||
7872b3ec55 | |||
27a0aebd12 | |||
366490516e | |||
9a92646d4d | |||
b002c49dac | |||
3f4ec9ba80 | |||
0290a5eacd | |||
744734a6a1 | |||
29f662f87c | |||
af21f9f10c | |||
efdc99b9d1 | |||
4458e63c1a | |||
3225745115 | |||
a325592106 | |||
01069ed583 | |||
0fc770bbb1 | |||
dfb79ef96e | |||
4ebffc8d43 | |||
c2dad08fef | |||
7c3ddf904c | |||
c3d73236e0 | |||
8a4da361fd | |||
57effe318b | |||
9325441693 | |||
180341576b | |||
e2533a93e3 | |||
14360bde78 | |||
d793265bed | |||
0a449e1e8e | |||
74ccc34c9c | |||
674cd1486d | |||
ce12e87b70 | |||
8f1324fdf3 | |||
3ab69046b0 | |||
6dc4bfaefe | |||
f460837f96 | |||
34d0d3e011 | |||
e57a488371 | |||
43be1e191f | |||
cfbcf0947a | |||
fcfba7f5e1 | |||
f4f9fabfd3 | |||
25208915eb | |||
eb975bf8fc | |||
21bbf49640 | |||
9339c7dff2 | |||
af0eb831a2 | |||
1fc9a1a54b | |||
3954ce2137 | |||
271de362cb | |||
d41474ebc8 | |||
5b0b3e30f4 | |||
48a95457b6 | |||
7c0b26174f | |||
f0145142a4 | |||
2848caff2e | |||
75f4a39ef2 | |||
f9f4d93191 | |||
9e05ad787f | |||
69050f7a56 | |||
de39fa0aea | |||
94ff77f2b2 | |||
bb7dc1ed4a | |||
c5e833ee79 | |||
4397591134 | |||
986c7b94f4 | |||
a6ef7387cf | |||
1743919cd4 | |||
131328b42c | |||
ad3b605148 | |||
f32e225fa6 | |||
95bdeacd93 | |||
07c2f6b810 | |||
8ff81f1648 | |||
c3ee43c228 | |||
d85da28ca7 | |||
042142396d | |||
fbc4ca89aa | |||
2e5d29064b | |||
ef0b8376d3 | |||
1fa1b74261 | |||
4f9e4116a2 | |||
82d8fda05f | |||
d4935263da | |||
e158d909fb | |||
de8147d5dd | |||
16f1791a9a | |||
8745c3f8c6 | |||
ec5b45cff6 | |||
1348197295 | |||
f2516854d8 | |||
062ca6e743 | |||
44b6997bb5 | |||
78b544f9ca | |||
81926b4450 | |||
a7ad71d492 | |||
18977f7265 | |||
8a88b44e98 | |||
c9e5fe42ba | |||
56dffbf514 | |||
0e1fac3773 | |||
e7c06880a8 | |||
39463a3202 | |||
36136f0f0f | |||
52e0845fc5 | |||
22e5b2869a | |||
fc3f32a4e0 | |||
3b0914e89e | |||
76cd9a7b25 | |||
0934bebf7b | |||
cd1a4c4749 | |||
8075273ec8 | |||
97b59be9bf | |||
b87ec4f3d9 | |||
3822358096 | |||
ba7e8cfe78 | |||
41978f1c59 | |||
e75e691404 | |||
a22216fd04 | |||
6900e16aa4 | |||
10c981b2a0 | |||
5f940df1b4 | |||
3f85918a0c | |||
e4299c09ea | |||
e864cf35f7 | |||
3652866660 | |||
0421004616 | |||
6936b034cb | |||
73ed4003a3 | |||
5cb8cdd511 | |||
195b5fdd1a | |||
d19b78b6cc | |||
9bbc05c3a7 | |||
7df3c86649 | |||
c6e0a923bb | |||
637fe1727b | |||
fd087bbeb8 | |||
2f515e1cc0 | |||
daf1a0a4bc | |||
bc8978182e | |||
84cd9e570f | |||
ead97a24bd | |||
415cde1629 | |||
b438312fde | |||
79ff2cb271 | |||
ed1464c405 | |||
f85631429b | |||
5ed56d1137 | |||
d7719d25b4 | |||
6267cccc3f | |||
fd5c4021f7 | |||
b8bf4d99ac | |||
0723eec508 | |||
7f01a12245 | |||
e1e3e5d953 | |||
18986faca8 | |||
2a68f8a90f | |||
659936577b | |||
85efc3b00c | |||
5efac45d46 | |||
c7dce280d7 | |||
04c6107196 | |||
54ce9b5dab | |||
cee955fb9d | |||
2e4b0daa48 | |||
e85ccfb47e | |||
b099f93c78 | |||
81afe397be | |||
f869c06aee | |||
75099b99d4 | |||
7b1b2a0f68 | |||
203c28df3d | |||
2e2c3cdec4 | |||
6f827c86a4 | |||
5aced90a3f | |||
4646f88e3a | |||
2b11cc1077 | |||
77b42eb085 | |||
7de067cd7a | |||
9da6df50b7 | |||
66b1623109 | |||
2432834f3d | |||
01fa483f95 | |||
1ddf47256f | |||
25fe32c3f8 | |||
ac9b8d03d7 | |||
8fdfb2c4f6 | |||
b1da136f77 | |||
9a6f85fa21 | |||
7308453a74 | |||
b798a17ef8 | |||
4b8899860e | |||
f46c8a0a0f | |||
48832f9ac3 | |||
9c798fc2e2 | |||
4704587f0a | |||
58e6b63fd7 | |||
3c76dfb584 | |||
10055d987d | |||
be49c60e83 | |||
14016e2f84 | |||
d7cb6f1cca | |||
0f63162254 | |||
21215dc537 | |||
20e147edfc | |||
1048dd516b | |||
42f44327f0 | |||
49200a4a9c | |||
7d71757de3 | |||
0fb492a70f | |||
7ccc1abb95 | |||
d61858e260 | |||
0ecd40f299 | |||
d9d4e74126 | |||
42d04bff61 | |||
f9cc29f014 | |||
992d359e79 | |||
1cc5427cbb | |||
6270a626fb | |||
40092b60fa | |||
5356b74490 | |||
e832ce5b4a | |||
a845ed88a7 | |||
560c1c3dc0 | |||
ecc5032bb2 | |||
325b359ff6 | |||
10fcc84379 | |||
149c29963d | |||
546c39a98e | |||
13223817a1 | |||
1b92314eb2 | |||
2b97808f1f | |||
8650446dcd | |||
7f24b89a80 | |||
e56ca73046 | |||
bf5062086c | |||
aa12167a6d | |||
9fa9f62d02 | |||
c9615b660e | |||
0ac51f479f | |||
37649fc77b | |||
ca4585eee9 | |||
83a1492cd4 | |||
a1af694acb | |||
6330c0f0d7 | |||
224c569ed1 | |||
c608987526 | |||
0c8f37ca19 | |||
aca67d6eae | |||
5dea0312ac | |||
f074007f67 | |||
88818ece29 | |||
fa0fa28949 | |||
08e31f6fe8 | |||
b976adeefe | |||
53c53b98e6 | |||
a171e00280 | |||
46f94d7175 | |||
2e555cac22 | |||
0d91b3286a | |||
396432b873 | |||
15c58434e8 | |||
daad1bdd25 | |||
c60966c725 | |||
fb57d8c3ce | |||
799ce74f65 | |||
8e38d7ceb4 | |||
a1c22e8071 | |||
6d8acf54d6 | |||
a500a89138 | |||
c6d44e7a89 | |||
9eb3aad072 | |||
9355454953 | |||
6467f06c54 | |||
b9b4b5ea39 | |||
e23243565f | |||
d3420532ae | |||
ade1b9d4eb | |||
fc278d12fc | |||
8e5ec822dc | |||
26aac66a76 | |||
a562e90bdb | |||
a0f3698701 | |||
02163f9482 | |||
b74fe171e2 | |||
2785bb4d9b | |||
5eac84d3a3 | |||
a0a2ab6fcd | |||
7730ead8e4 | |||
8eee0dd14c | |||
7dd88d8d8f | |||
56d1d3e645 | |||
c2308675b2 | |||
cb866a1c05 | |||
95290e8331 | |||
f5e62c775b | |||
f533309b49 | |||
d1c70a7cb3 | |||
2f8590ca7a | |||
08badbde56 | |||
8e38da80e0 | |||
cd2e3350b0 | |||
a0d2790491 | |||
8ca99e5635 | |||
5a2563ca7f | |||
a23cd28531 | |||
58a967b59e | |||
9bf0c20198 | |||
6b7ac0e000 | |||
188c0a9a86 | |||
c49479c8ad | |||
2072b6e136 | |||
08d82390b0 | |||
b845a545e2 | |||
db958b2401 | |||
7266420eec | |||
f36fbe7a76 | |||
8e279b110c | |||
d626870e46 | |||
df49b094d5 | |||
7d17bf7f2a | |||
e51f3dd1ae | |||
b810b88c6c | |||
39b34ff4ed | |||
f72fd63113 | |||
97eedc2c9f | |||
db222c53e3 | |||
61e919b88d | |||
d14040c142 | |||
13a3a581d8 | |||
f6dbae1cef | |||
ccbcda86ac | |||
b74e8cf756 | |||
8f8266f15d | |||
ab8d3f5813 | |||
08220dbea5 | |||
3b2cf2f1de | |||
c3beca27be | |||
28b820241f | |||
e985224092 | |||
f1c467aa7d | |||
ae7cfe90ab | |||
718a36ddd0 | |||
c0b903d79c | |||
48eaf906b0 | |||
f42fde970a | |||
59afebaa57 | |||
3e06e45054 | |||
fe55acb268 | |||
710dbb51f4 | |||
d426d66819 | |||
265cddc38b | |||
21b91ac8f7 | |||
e656813844 | |||
e8730f74be | |||
6d611d7d05 | |||
392f3a16f1 | |||
2b2e12b290 | |||
73cc75fe66 | |||
cc186fc8b3 | |||
632ad81b94 | |||
1d243910ae | |||
e624649cd8 | |||
e6ca07e9b5 | |||
b3d6435772 | |||
806474c8c6 | |||
1524fb4499 | |||
14b70ff35e | |||
c36a900627 | |||
d3befb5b86 | |||
7f0ce1f802 | |||
8342ad9175 | |||
acb2407654 | |||
b8a4f0c012 | |||
57bb3b231c | |||
e5d626e0fd | |||
e2c4c913ff | |||
09f97915d6 | |||
81328b2667 | |||
0d8affc68d | |||
da77d278fb | |||
5e9f6f3542 | |||
f337470f09 | |||
636224d0c8 | |||
b28b3ef4ff | |||
9e2e102ec4 | |||
9e16b83202 | |||
cbd40d49c1 | |||
1d051648b7 | |||
49cf804914 | |||
0f6ad75536 | |||
56eea18b2d | |||
b3698846c6 | |||
6806d96baa | |||
dc3b3077c2 | |||
936ae64ca3 | |||
3a0a5dbd7f | |||
ed4430ae7d | |||
9a0e4e35d9 | |||
5715dd2058 | |||
da4c132f9d | |||
303a617f9e | |||
3116ec9cb8 | |||
b3f4eab075 | |||
c98593b47b | |||
2c49d61682 | |||
21f5c94cff | |||
834ba4afab | |||
d690cd6b7b | |||
937bd07daa | |||
559b822111 | |||
7afb4c6b11 | |||
1c98a3a33d | |||
3320eb284e | |||
919fb60558 | |||
7796589105 | |||
de6d3198ff | |||
f1e971d047 | |||
acd98aad32 | |||
b0c810398c | |||
03a0044745 | |||
15684efdce | |||
b67a962d12 | |||
339cedadf7 | |||
e19d730fb7 | |||
649497e54f | |||
40eee0924a | |||
bbf5fb3c30 | |||
346cdf2431 | |||
030cb09af8 | |||
9f734349da | |||
2d5a861df0 | |||
061f428a54 | |||
9a539fd350 | |||
4149fe10e9 | |||
1014083160 | |||
dfa3167c18 | |||
7e09efb9a3 | |||
fb736c0d0f | |||
e4a7263e9b | |||
c52926f2b0 | |||
b6138b36be | |||
04bce3ae00 | |||
68ca162dd3 | |||
100bb02cd5 | |||
856249d52c | |||
309d6fdfe0 | |||
f289420364 | |||
f05e85de5f | |||
4138849546 | |||
7052e4e1dc | |||
ffb3e4f1fb | |||
297834be66 | |||
5924f1730c | |||
adc6bea4dc | |||
ef431f688f | |||
c8923af573 | |||
3d33ecf397 | |||
82d38da18e | |||
200e259b82 | |||
2d1f4e5e0a | |||
7fe64612ad | |||
5e452a679e | |||
10be8aec82 | |||
0e1a1fd2cd | |||
3f07010de8 | |||
2e45c8b190 | |||
b4f4401cdc | |||
a6b92a0dd5 | |||
2f3238c65e | |||
65f5a38b4a | |||
271cbf682f | |||
a634593903 | |||
af94de93d1 | |||
35f669aa15 | |||
4795bd8108 | |||
29aed99fd1 | |||
aa4519ac30 | |||
752133b01c | |||
fe0c21ba08 | |||
90904a6b5e | |||
8e3f7ea68d | |||
a239104a28 | |||
3bc232e1da | |||
a1ee09cd85 | |||
b898cc030c | |||
0602353dd2 | |||
9d406923ae | |||
aa8565e3cc | |||
5de330b1f9 | |||
66597aed46 | |||
3071826f06 | |||
c3684eb064 | |||
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 |
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -287,3 +287,9 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
/BTCPayServer/Build/dockerfiles
|
||||
|
||||
# Bundling JS/CSS
|
||||
BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
||||
.vscode
|
||||
|
@ -1,15 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,7 +1,12 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -45,7 +50,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
public Uri LTCNBXplorerUri { get; set; }
|
||||
|
||||
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
@ -63,11 +68,14 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public bool MockRates { get; set; } = true;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
@ -84,6 +92,8 @@ namespace BTCPayServer.Tests
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
@ -91,13 +101,12 @@ 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 });
|
||||
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
@ -111,21 +120,85 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
|
||||
if (MockRates)
|
||||
{
|
||||
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
|
||||
rateProvider.Providers.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
BidAsk = new BidAsk(5000m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(4500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
|
||||
BidAsk = new BidAsk(0.001m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.Providers.Add("coinaverage", coinAverageMock);
|
||||
|
||||
var bitflyerMock = new MockRateProvider();
|
||||
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bitflyer",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
|
||||
BidAsk = new BidAsk(700000m)
|
||||
});
|
||||
rateProvider.Providers.Add("bitflyer", bitflyerMock);
|
||||
|
||||
var quadrigacx = new MockRateProvider();
|
||||
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "quadrigacx",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(6000m)
|
||||
});
|
||||
rateProvider.Providers.Add("quadrigacx", quadrigacx);
|
||||
|
||||
var bittrex = new MockRateProvider();
|
||||
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bittrex",
|
||||
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
|
||||
BidAsk = new BidAsk(0.004m)
|
||||
});
|
||||
rateProvider.Providers.Add("bittrex", bittrex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
return _Host.Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null) where T : Controller
|
||||
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
@ -133,7 +206,11 @@ namespace BTCPayServer.Tests
|
||||
context.Request.Protocol = "http";
|
||||
if (userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
|
||||
}
|
||||
if (storeId != null)
|
||||
{
|
||||
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
|
25
BTCPayServer.Tests/ChargeTester.cs
Normal file
25
BTCPayServer.Tests/ChargeTester.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChargeTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public ChargeTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
|
||||
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
|
||||
@ -9,4 +9,4 @@ RUN dotnet restore
|
||||
# copies the rest of your code
|
||||
COPY . ../.
|
||||
|
||||
ENTRYPOINT ["dotnet", "test"]
|
||||
ENTRYPOINT ["dotnet", "test"]
|
||||
|
@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Eclair;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class EclairTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
|
||||
{
|
||||
this.parent = parent;
|
||||
//RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
|
||||
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public EclairRPCClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
NodeInfo _NodeInfo;
|
||||
public async Task<NodeInfo> GetNodeInfoAsync()
|
||||
{
|
||||
if (_NodeInfo != null)
|
||||
return _NodeInfo;
|
||||
var info = await RPC.GetInfoAsync();
|
||||
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
|
||||
return _NodeInfo;
|
||||
}
|
||||
|
||||
public NodeInfo GetNodeInfo()
|
||||
{
|
||||
return GetNodeInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class LightningDTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
}
|
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
public class LndMockTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndClient(Swagger, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
@ -71,7 +71,11 @@ namespace BTCPayServer.Tests.Logging
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if (msg != null)
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
try
|
||||
{
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
|
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
18
BTCPayServer.Tests/Mocks/MockRateProvider.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ docker-compose down
|
||||
If you want to stop, and remove all existing data
|
||||
|
||||
```
|
||||
docker-compose down -v
|
||||
docker-compose down --v
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
@ -35,14 +35,50 @@ You can run the tests inside a container by running
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
## Send commands to bitcoind
|
||||
## How to manually test payments
|
||||
|
||||
### Using the test bitcoin-cli
|
||||
|
||||
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
|
||||
```
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
### Using the test litecoin-cli
|
||||
|
||||
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
|
||||
|
||||
### Using the test lightning-cli
|
||||
|
||||
If you are using Linux:
|
||||
```
|
||||
./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
|
||||
```
|
||||
|
||||
If you get this message:
|
||||
|
||||
```
|
||||
{ "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } }
|
||||
```
|
||||
|
||||
Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry.
|
||||
|
||||
## FAQ
|
||||
|
||||
`docker-compose up dev` failed or tests are not passing, what should I do?
|
||||
|
||||
1. Run `docker-compose down --v` (this will reset your test environment)
|
||||
2. Run `docker-compose pull` (this will ensure you have the lastest images)
|
||||
3. Run again with `docker-compose up dev`
|
||||
|
||||
If you still have issues, try to restart docker.
|
||||
|
179
BTCPayServer.Tests/RateRulesTest.cs
Normal file
179
BTCPayServer.Tests/RateRulesTest.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using Xunit;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class RateRulesTest
|
||||
{
|
||||
[Fact]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
// Check happy path
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("// Some cool comments");
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("// Some other cool comments");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X);");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
|
||||
Assert.False(RateRules.TryParse("DPW*&W&#hdi&#&3JJD", out var rules));
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
Assert.Equal(
|
||||
"// Some cool comments\n" +
|
||||
"DOGE_X = DOGE_BTC * BTC_X * 1.1;\n" +
|
||||
"DOGE_BTC = bittrex(DOGE_BTC);\n" +
|
||||
"// Some other cool comments\n" +
|
||||
"BTC_USD = gdax(BTC_USD);\n" +
|
||||
"BTC_X = coinbase(BTC_X);\n" +
|
||||
"X_X = coinaverage(X_X) * 1.02;",
|
||||
rules.ToString());
|
||||
var tests = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
rules.Spread = 0.2m;
|
||||
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
|
||||
////////////////
|
||||
|
||||
// Check errors conditions
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = LTC_CAD * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("LTC_CHF = LTC_CHF * 1.01");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
tests = new[]
|
||||
{
|
||||
(Pair: "LTC_CAD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "ERR_NO_RULE_MATCH(LTC_CAD) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "LTC_CHF", Expected: "ERR_TOO_MUCH_NESTED_CALLS(LTC_CHF) * 1.01"),
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
|
||||
}
|
||||
//////////////////
|
||||
|
||||
// Check if we can resolve exchange rates
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X * 1.1");
|
||||
builder.AppendLine("DOGE_BTC = Bittrex(DOGE_BTC)");
|
||||
builder.AppendLine("BTC_usd = GDax(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = Coinbase(BTC_X)");
|
||||
builder.AppendLine("X_X = CoinAverage(X_X) * 1.02");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
|
||||
var tests2 = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||
};
|
||||
foreach (var test in tests2)
|
||||
{
|
||||
var rule = rules.GetRuleFor(CurrencyPair.Parse(test.Pair));
|
||||
Assert.Equal(test.Expected, rule.ToString());
|
||||
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
|
||||
}
|
||||
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
|
||||
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
|
||||
rule2.Reevaluate();
|
||||
Assert.True(rule2.HasError);
|
||||
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("DOGE_X = DOGE_BTC * BTC_X");
|
||||
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
|
||||
builder.AppendLine("DOGE_BTC = 2000");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rules.Spread = 0.1m;
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
|
||||
|
||||
// Test inverse
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
|
||||
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
|
||||
////////
|
||||
|
||||
// Make sure kraken is not converted to CurrencyPair
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
|
||||
// Make sure can handle pairs
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(6000m, rule2.BidAsk.Bid);
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("EUR_CDNT = 10");
|
||||
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
|
||||
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / 10", rule2.ToString(false));
|
||||
|
||||
// Make sure an inverse can be solved on an exchange
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,11 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Eclair;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -32,6 +36,36 @@ namespace BTCPayServer.Tests
|
||||
public ServerTester(string scope)
|
||||
{
|
||||
_Directory = scope;
|
||||
if (Directory.Exists(_Directory))
|
||||
Utils.DeleteDirectory(_Directory);
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
IntegratedLightning = MerchantCharge.Client.Uri
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
|
||||
}
|
||||
|
||||
public bool Dockerized
|
||||
@ -41,62 +75,46 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Directory.Exists(_Directory))
|
||||
Utils.DeleteDirectory(_Directory);
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.Start();
|
||||
|
||||
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This will setup a channel going from customer to merchant
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
/// <returns></returns>
|
||||
public Task EnsureConnectedToDestinations()
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
}
|
||||
|
||||
public async Task PrepareLightningAsync()
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
{
|
||||
// Activate segwit
|
||||
var blockCount = ExplorerNode.GetBlockCountAsync();
|
||||
// Fetch node info, but that in cache
|
||||
var merchant = MerchantEclair.GetNodeInfoAsync();
|
||||
var customer = CustomerEclair.GetNodeInfoAsync();
|
||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
|
||||
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
|
||||
|
||||
// Mine until segwit is activated
|
||||
if (blockCount.Result <= 432)
|
||||
{
|
||||
ExplorerNode.Generate(433 - blockCount.Result);
|
||||
}
|
||||
yield return CustomerLightningD;
|
||||
}
|
||||
|
||||
public EclairTester MerchantEclair { get; set; }
|
||||
public EclairTester CustomerEclair { get; set; }
|
||||
private IEnumerable<ILightningClient> GetLightningDestClients()
|
||||
{
|
||||
yield return MerchantLightningD;
|
||||
yield return MerchantLnd.Client;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task SendLightningPaymentAsync(Invoice invoice)
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public ILightningClient CustomerLightningD { get; set; }
|
||||
|
||||
public ILightningClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
@ -128,106 +146,18 @@ namespace BTCPayServer.Tests
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
class MockHttpRequest : HttpRequest
|
||||
{
|
||||
Uri serverUri;
|
||||
public MockHttpRequest(Uri serverUri)
|
||||
{
|
||||
this.serverUri = serverUri;
|
||||
}
|
||||
public override HttpContext HttpContext => throw new NotImplementedException();
|
||||
|
||||
public override string Method
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Scheme
|
||||
{
|
||||
get => serverUri.Scheme;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override bool IsHttps
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override HostString Host
|
||||
{
|
||||
get => new HostString(serverUri.Host, serverUri.Port);
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString PathBase
|
||||
{
|
||||
get => "";
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString Path
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override QueryString QueryString
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override IQueryCollection Query
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Protocol
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IHeaderDictionary Headers => throw new NotImplementedException();
|
||||
|
||||
public override IRequestCookieCollection Cookies
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override long? ContentLength
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string ContentType
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override Stream Body
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool HasFormContentType => throw new NotImplementedException();
|
||||
|
||||
public override IFormCollection Form
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -11,6 +12,11 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -41,37 +47,28 @@ namespace BTCPayServer.Tests
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
var store = await CreateStoreAsync();
|
||||
await CreateStoreAsync();
|
||||
var store = this.GetController<StoresController>();
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
public StoresController CreateStore(string cryptoCode = null)
|
||||
public void CreateStore()
|
||||
{
|
||||
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public string CryptoCode { get; set; } = "BTC";
|
||||
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
cryptoCode = cryptoCode ?? CryptoCode;
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
}
|
||||
|
||||
public async Task CreateStoreAsync()
|
||||
{
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
}, "Save");
|
||||
return store;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
@ -80,17 +77,21 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string crytoCode)
|
||||
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
|
||||
{
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
|
||||
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
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);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = crytoCode,
|
||||
DerivationSchemeFormat = crytoCode,
|
||||
DerivationScheme = derivation.ToString(),
|
||||
}, "Save");
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
@ -120,5 +121,33 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = connectionString,
|
||||
SkipPortTest = true
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,5 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -13,25 +9,27 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
[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);
|
||||
// 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()
|
||||
// {
|
||||
|
||||
// go to invoice.Url
|
||||
Console.WriteLine(invoice.Url);
|
||||
}
|
||||
// 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]
|
||||
|
@ -1 +1 @@
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
|
||||
|
3
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
3
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
|
@ -1,7 +1,7 @@
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
@ -17,17 +17,24 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- dev
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
volumes:
|
||||
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
|
||||
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
image: nicolasdorier/docker-bitcoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
@ -35,9 +42,28 @@ services:
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.3
|
||||
image: nicolasdorier/nbxplorer:1.0.2.31
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -61,8 +87,7 @@ services:
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
image: nicolasdorier/docker-bitcoin:0.16.3
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
@ -72,15 +97,85 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6.1-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
dev-bitcoind-poll=1
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "customer_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.3
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
links:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:v0.6.1-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=merchant_lightningd
|
||||
network=regtest
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
ports:
|
||||
- "30993:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
image: nicolasdorier/docker-litecoin:0.15.1
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
@ -102,3 +197,64 @@ services:
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.5-beta
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noseedbackup=1
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
customer_lnd_datadir:
|
||||
merchant_lnd_datadir:
|
||||
|
1
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
1
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
|
3
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
3
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
|
@ -1 +1 @@
|
||||
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
|
||||
|
3
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
3
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
|
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
|
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
|
@ -1,35 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitIdentity : IIdentity
|
||||
{
|
||||
public BitIdentity(PubKey key)
|
||||
{
|
||||
PubKey = key;
|
||||
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
|
||||
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
|
||||
}
|
||||
string _Name;
|
||||
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
if (sin == null)
|
||||
return Array.Empty<BitTokenEntity>();
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
@ -43,6 +45,46 @@ namespace BTCPayServer.Authentication
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GenerateLegacyAPIKey(string storeId)
|
||||
{
|
||||
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
|
||||
// as good as possible. The string below got generated for me.
|
||||
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
|
||||
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
|
||||
var generated = new char[chars.Length];
|
||||
for (int i = 0; i < generated.Length; i++)
|
||||
{
|
||||
generated[i] = chars[rand.Next(0, generated.Length)];
|
||||
}
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
@ -13,34 +14,28 @@ namespace BTCPayServer
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||
chainType == NetworkType.Regtest ? 23002 :
|
||||
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
@ -49,7 +44,8 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public IRateProvider DefaultRateProvider { get; set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
@ -61,13 +57,23 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public string LightningImagePath { get; set; }
|
||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
}
|
||||
|
||||
internal KeyPath GetRootKeyPath()
|
||||
{
|
||||
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
|
||||
.Derive(CoinType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,19 +14,18 @@ namespace BTCPayServer
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProvider("BTC");
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
DisplayName = "Bitcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinGold()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "BGold",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoingold",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Dash.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitDash()
|
||||
{
|
||||
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dash",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://insight.dash.org/insight/tx/{0}"
|
||||
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dash",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DASH_X = DASH_BTC * BTC_X",
|
||||
"DASH_BTC = bittrex(DASH_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dash.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
|
||||
: new KeyPath("1'"),
|
||||
MinFee = Money.Satoshis(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitDogecoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dogecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DOGE_X = DOGE_BTC * BTC_X",
|
||||
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Feathercoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "feathercoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FTC_X = FTC_BTC * BTC_X",
|
||||
"FTC_BTC = bittrex(FTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/feathercoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Groestlcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
@ -11,20 +12,19 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
|
||||
var ltcRate = new CoinAverageRateProvider("LTC");
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType)
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitMonacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Monacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "monacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = zaif(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitPolis()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Polis",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "polis",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"POLIS_X = POLIS_BTC * BTC_X",
|
||||
"POLIS_BTC = cryptopia(POLIS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/polis.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Ufo",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "ufo",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"UFO_X = UFO_BTC * BTC_X",
|
||||
"UFO_BTC = coinexchange(UFO_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/ufo.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitViacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Viacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "viacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"VIA_X = VIA_BTC * BTC_X",
|
||||
"VIA_BTC = bittrex(VIA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/viacoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -25,11 +25,47 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
NetworkType = filtered.NetworkType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
|
||||
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||
foreach (var network in filtered._Networks)
|
||||
{
|
||||
if(cryptoCodes.Contains(network.Key))
|
||||
{
|
||||
_Networks.Add(network.Key, network.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkType NetworkType { get; private set; }
|
||||
public BTCPayNetworkProvider(NetworkType networkType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitDash();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keep only the specified crypto
|
||||
/// </summary>
|
||||
/// <param name="cryptoCodes">Crypto to support</param>
|
||||
/// <returns></returns>
|
||||
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
|
||||
{
|
||||
return new BTCPayNetworkProvider(this, cryptoCodes);
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
@ -43,7 +79,7 @@ namespace BTCPayServer
|
||||
|
||||
public void Add(BTCPayNetwork network)
|
||||
{
|
||||
_Networks.Add(network.CryptoCode, network);
|
||||
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
|
||||
}
|
||||
|
||||
public IEnumerable<BTCPayNetwork> GetAll()
|
||||
@ -51,9 +87,18 @@ namespace BTCPayServer
|
||||
return _Networks.Values.ToArray();
|
||||
}
|
||||
|
||||
public bool Support(string cryptoCode)
|
||||
{
|
||||
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||
if(!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network))
|
||||
{
|
||||
if (cryptoCode == "XBT")
|
||||
return GetNetwork("BTC");
|
||||
}
|
||||
return network;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.16</Version>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.106</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\css\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Remove="Build\dockerfiles\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="wwwroot\css\**" />
|
||||
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<EmbeddedResource Remove="Build\dockerfiles\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="wwwroot\css\**" />
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\dockerfiles\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="wwwroot\css\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
@ -17,42 +33,44 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.52" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.16" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.48" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.19" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" PrivateAssets="All" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
<None Include="wwwroot\js\site.js" />
|
||||
<None Include="wwwroot\js\site.min.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
|
||||
<None Include="wwwroot\checkout\js\core.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
@ -98,5 +116,56 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\Apps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.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\LNDGRPCServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Maintenance.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Services.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
72
BTCPayServer/BTCPayServer.ruleset
Normal file
72
BTCPayServer/BTCPayServer.ruleset
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
|
||||
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
|
||||
<Name Resource="MinimumRecommendedRules_Name" />
|
||||
<Description Resource="MinimumRecommendedRules_Description" />
|
||||
</Localization>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1009" Action="Warning" />
|
||||
<Rule Id="CA1016" Action="Warning" />
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1049" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1061" Action="Warning" />
|
||||
<Rule Id="CA1063" Action="Warning" />
|
||||
<Rule Id="CA1065" Action="Warning" />
|
||||
<Rule Id="CA1301" Action="Warning" />
|
||||
<Rule Id="CA1400" Action="Warning" />
|
||||
<Rule Id="CA1401" Action="Warning" />
|
||||
<Rule Id="CA1403" Action="Warning" />
|
||||
<Rule Id="CA1404" Action="Warning" />
|
||||
<Rule Id="CA1405" Action="Warning" />
|
||||
<Rule Id="CA1410" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1821" Action="Warning" />
|
||||
<Rule Id="CA1900" Action="Warning" />
|
||||
<Rule Id="CA1901" Action="Warning" />
|
||||
<Rule Id="CA2002" Action="Warning" />
|
||||
<Rule Id="CA2100" Action="Warning" />
|
||||
<Rule Id="CA2101" Action="Warning" />
|
||||
<Rule Id="CA2108" Action="Warning" />
|
||||
<Rule Id="CA2111" Action="Warning" />
|
||||
<Rule Id="CA2112" Action="Warning" />
|
||||
<Rule Id="CA2114" Action="Warning" />
|
||||
<Rule Id="CA2116" Action="Warning" />
|
||||
<Rule Id="CA2117" Action="Warning" />
|
||||
<Rule Id="CA2122" Action="Warning" />
|
||||
<Rule Id="CA2123" Action="Warning" />
|
||||
<Rule Id="CA2124" Action="Warning" />
|
||||
<Rule Id="CA2126" Action="Warning" />
|
||||
<Rule Id="CA2131" Action="Warning" />
|
||||
<Rule Id="CA2132" Action="Warning" />
|
||||
<Rule Id="CA2133" Action="Warning" />
|
||||
<Rule Id="CA2134" Action="Warning" />
|
||||
<Rule Id="CA2137" Action="Warning" />
|
||||
<Rule Id="CA2138" Action="Warning" />
|
||||
<Rule Id="CA2140" Action="Warning" />
|
||||
<Rule Id="CA2141" Action="Warning" />
|
||||
<Rule Id="CA2146" Action="Warning" />
|
||||
<Rule Id="CA2147" Action="Warning" />
|
||||
<Rule Id="CA2149" Action="Warning" />
|
||||
<Rule Id="CA2200" Action="Warning" />
|
||||
<Rule Id="CA2202" Action="Warning" />
|
||||
<Rule Id="CA2207" Action="Warning" />
|
||||
<Rule Id="CA2212" Action="Warning" />
|
||||
<Rule Id="CA2213" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="Warning" />
|
||||
<Rule Id="CA2216" Action="Warning" />
|
||||
<Rule Id="CA2220" Action="Warning" />
|
||||
<Rule Id="CA2229" Action="Warning" />
|
||||
<Rule Id="CA2231" Action="Warning" />
|
||||
<Rule Id="CA2232" Action="Warning" />
|
||||
<Rule Id="CA2235" Action="Warning" />
|
||||
<Rule Id="CA2236" Action="Warning" />
|
||||
<Rule Id="CA2237" Action="Warning" />
|
||||
<Rule Id="CA2238" Action="Warning" />
|
||||
<Rule Id="CA2240" Action="Warning" />
|
||||
<Rule Id="CA2241" Action="Warning" />
|
||||
<Rule Id="CA2242" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
|
||||
</RuleSet>
|
@ -10,6 +10,11 @@ using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -22,7 +27,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public ChainType ChainType
|
||||
public NetworkType NetworkType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -50,36 +55,170 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
{
|
||||
if (supportedChains.Contains(net.CryptoCode))
|
||||
if (NetworkProvider.GetNetwork(chain) == null)
|
||||
throw new ConfigException($"Invalid chains \"{chain}\"");
|
||||
}
|
||||
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in NetworkProvider.GetAll())
|
||||
{
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
{
|
||||
validChains.Add(net.CryptoCode);
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
|
||||
if(!string.IsNullOrEmpty(invalidChains))
|
||||
throw new ConfigException($"Invalid chains {invalidChains}");
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", 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");
|
||||
}
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
var settings = new SSHSettings();
|
||||
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
|
||||
if (settings.Server != null)
|
||||
{
|
||||
var parts = settings.Server.Split(':');
|
||||
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
|
||||
{
|
||||
settings.Port = port;
|
||||
settings.Server = parts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Port = 22;
|
||||
}
|
||||
|
||||
parts = settings.Server.Split('@');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
settings.Username = parts[0];
|
||||
settings.Server = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Username = "root";
|
||||
}
|
||||
}
|
||||
else if (externalUrl != null)
|
||||
{
|
||||
settings.Port = 22;
|
||||
settings.Username = "root";
|
||||
settings.Server = externalUrl.DnsSafeHost;
|
||||
}
|
||||
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
|
||||
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
|
||||
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
|
||||
return settings;
|
||||
}
|
||||
|
||||
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
|
||||
{
|
||||
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
|
||||
}
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
@ -90,5 +229,50 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
|
||||
if (str == null)
|
||||
return defaultValue;
|
||||
if (typeof(T) == typeof(bool))
|
||||
@ -27,17 +27,24 @@ namespace BTCPayServer.Configuration
|
||||
throw new FormatException();
|
||||
}
|
||||
else if (typeof(T) == typeof(Uri))
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
}
|
||||
else if (typeof(T) == typeof(string))
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":");
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
if (separator == -1)
|
||||
throw new FormatException();
|
||||
var ip = str.Substring(0, separator);
|
||||
var port = str.Substring(separator + 1);
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (typeof(T) == typeof(int))
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
@ -27,17 +27,26 @@ namespace BTCPayServer.Configuration
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--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("--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);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
app.Option("--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);
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -45,12 +54,12 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
@ -66,7 +75,7 @@ namespace BTCPayServer.Configuration
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
public static ChainType GetChainType(IConfiguration conf)
|
||||
public static NetworkType GetNetworkType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
@ -76,17 +85,18 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
return n.NetworkType;
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
|
||||
|
||||
return net;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var networkType = GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
@ -99,10 +109,12 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
@ -111,7 +123,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -23,12 +25,13 @@ namespace BTCPayServer.Controllers
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
@ -51,8 +54,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
@ -76,6 +79,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
Policies = new Newtonsoft.Json.Linq.JArray(),
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
|
@ -16,10 +16,13 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
@ -68,6 +71,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;
|
||||
@ -86,7 +90,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.");
|
||||
@ -150,7 +154,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
@ -204,7 +208,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
@ -235,23 +239,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)
|
||||
{
|
||||
@ -273,7 +279,8 @@ namespace BTCPayServer.Controllers
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
if(logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
|
138
BTCPayServer/Controllers/AppsController.PointOfSale.cs
Normal file
138
BTCPayServer/Controllers/AppsController.PointOfSale.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class PointOfSaleSettings
|
||||
{
|
||||
public PointOfSaleSettings()
|
||||
{
|
||||
Title = "My awesome Point of Sale";
|
||||
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";
|
||||
ShowCustomAmount = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
|
||||
var encoder = HtmlEncoder.Default;
|
||||
if (settings.ShowCustomAmount)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example1 = builder.ToString();
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example2 = builder.ToString();
|
||||
}
|
||||
catch { }
|
||||
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
|
||||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
_AppsHelper.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
private async Task UpdateAppSettings(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(app);
|
||||
ctx.Entry<AppData>(app).State = EntityState.Modified;
|
||||
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
205
BTCPayServer/Controllers/AppsController.cs
Normal file
205
BTCPayServer/Controllers/AppsController.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class AppsController : Controller
|
||||
{
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteAppPost(string appId)
|
||||
{
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must have created at least one store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var vm = new CreateAppViewModel();
|
||||
vm.SetStores(stores);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must own at least one store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var selectedStore = vm.SelectedStore;
|
||||
vm.SetStores(stores);
|
||||
vm.SelectedStore = selectedStore;
|
||||
|
||||
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType))
|
||||
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (!stores.Any(s => s.Id == selectedStore))
|
||||
{
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var appData = new AppData() { Id = id };
|
||||
appData.StoreDataId = selectedStore;
|
||||
appData.Name = vm.Name;
|
||||
appData.AppType = appType.ToString();
|
||||
ctx.Apps.Add(appData);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteApp(string appId)
|
||||
{
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Delete app {appData.Name} ({appData.AppType})",
|
||||
Description = "This app will be removed from this store",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
private async 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();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
185
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
185
BTCPayServer/Controllers/AppsPublicController.cs
Normal file
@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _AppsHelper.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 = _AppsHelper.Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = _AppsHelper.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 _AppsHelper.GetStore(app);
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
CurrencyNameTable _Currencies;
|
||||
|
||||
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_Currencies = currencies;
|
||||
|
||||
}
|
||||
|
||||
public async Task<AppData> GetApp(string appId, AppType appType)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(AppData app)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
|
||||
{
|
||||
var input = new StringReader(template);
|
||||
YamlStream stream = new YamlStream();
|
||||
stream.Load(input);
|
||||
var root = (YamlMappingNode)stream.Documents[0].RootNode;
|
||||
return root
|
||||
.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Select(c => new ViewPointOfSaleViewModel.Item()
|
||||
{
|
||||
Id = c.Key,
|
||||
Title = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "title")
|
||||
.FirstOrDefault()?.Value?.Value ?? c.Key,
|
||||
Price = c.Value.Children
|
||||
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
|
||||
.Where(kv => kv.Value != null)
|
||||
.Where(cc => cc.Key == "price")
|
||||
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
|
||||
{
|
||||
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
|
||||
Formatted = FormatCurrency(cc.Value.Value, currency)
|
||||
})
|
||||
.Single()
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public string FormatCurrency(string price, string currency)
|
||||
{
|
||||
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
return _Currencies.GetCurrencyData(currency, useFallback);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -1,41 +1,32 @@
|
||||
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.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
@ -44,21 +35,17 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
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)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
@ -77,8 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
@ -87,55 +73,14 @@ namespace BTCPayServer.Controllers
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status,
|
||||
StoreId = store.Id
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
|
||||
{
|
||||
if (facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
return store;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,21 @@
|
||||
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;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
@ -23,11 +23,12 @@ namespace BTCPayServer.Controllers
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
|
||||
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 cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
@ -35,7 +36,7 @@ namespace BTCPayServer.Controllers
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
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);
|
||||
@ -69,16 +70,48 @@ namespace BTCPayServer.Controllers
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(network))
|
||||
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);
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
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.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.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -31,7 +31,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId,
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
@ -48,14 +47,17 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
"low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -63,163 +65,305 @@ namespace BTCPayServer.Controllers
|
||||
Events = invoice.Events
|
||||
};
|
||||
|
||||
foreach (var data in invoice.GetCryptoData(null))
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
|
||||
var accounting = data.Value.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Address = data.Value.DepositAddress.ToString();
|
||||
cryptoPayment.Rate = FormatCurrency(data.Value);
|
||||
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}";
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Select(async payment =>
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
}
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Addresses = invoice.HistoricalAddresses;
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
await Task.WhenAll(onChainPayments);
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
||||
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var type = paymentMethodId.PaymentType.ToString();
|
||||
switch (paymentMethodId.PaymentType)
|
||||
{
|
||||
case PaymentTypes.BTCLike:
|
||||
type = "On-Chain";
|
||||
break;
|
||||
case PaymentTypes.LightningLike:
|
||||
type = "Off-Chain";
|
||||
break;
|
||||
}
|
||||
return $"{paymentMethodId.CryptoCode} ({type})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{cryptoCode}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, cryptoCode);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
|
||||
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
bool isDefaultCrypto = false;
|
||||
if (cryptoCode == null)
|
||||
{
|
||||
cryptoCode = store.GetDefaultCrypto();
|
||||
if (paymentMethodIdStr == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
|
||||
if(!invoice.Support(network))
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if(!isDefaultCrypto)
|
||||
if (!isDefaultCrypto)
|
||||
return null;
|
||||
network = invoice.GetCryptoData(_NetworkProvider).First().Value.Network;
|
||||
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 cryptoData = invoice.GetCryptoData(network, _NetworkProvider);
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
|
||||
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = cryptoData.Calculate();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BtcAddress = cryptoData.DepositAddress,
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(cryptoData),
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
|
||||
TxCount = accounting.TxCount,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
throw new NotSupportedException(),
|
||||
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
|
||||
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
|
||||
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}",
|
||||
AvailableCryptos = invoice.GetCryptoData(_NetworkProvider)
|
||||
.Where(i => i.Value.Network != null)
|
||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
CryptoCode = kv.Key,
|
||||
CryptoImage = "/" + kv.Value.Network.CryptoImagePath,
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
|
||||
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
CryptoImage = GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
return model;
|
||||
}
|
||||
|
||||
private string FormatCurrency(CryptoData cryptoData)
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(cryptoData.Rate, currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " (via Lightning)";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString());
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00"));
|
||||
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
|
||||
return builder.ToString();
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
}
|
||||
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})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{cryptoCode}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
||||
{
|
||||
var model = await GetInvoiceModel(invoiceId, cryptoCode);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -239,7 +383,8 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -250,7 +395,7 @@ namespace BTCPayServer.Controllers
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await CloseSocket(webSocket);
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
@ -269,21 +414,6 @@ namespace BTCPayServer.Controllers
|
||||
catch { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
private static async Task CloseSocket(WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
@ -298,9 +428,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
{
|
||||
var model = new InvoicesModel();
|
||||
var filterString = new SearchString(searchTerm);
|
||||
@ -310,16 +440,23 @@ namespace BTCPayServer.Controllers
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Status = filterString.Filters.TryGet("status"),
|
||||
StoreId = filterString.Filters.TryGet("storeid")
|
||||
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,
|
||||
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}"
|
||||
});
|
||||
}
|
||||
@ -331,34 +468,51 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice()
|
||||
{
|
||||
var stores = await GetStores(GetUserId());
|
||||
var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
|
||||
if (stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
return View(new CreateInvoiceModel() { Stores = stores });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
model.Stores = await GetStores(GetUserId(), model.StoreId);
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if (store == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
if (store.GetDerivationStrategies(_NetworkProvider).Count() == 0)
|
||||
StatusMessage = null;
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
@ -383,20 +537,15 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
catch (RateUnavailableException)
|
||||
catch (BitpayHttpException ex)
|
||||
{
|
||||
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
|
||||
ModelState.TryAddModelError(nameof(model.Currency), $"Error: {ex.Message}");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SelectList> GetStores(string userId, string storeId = null)
|
||||
{
|
||||
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
@ -410,12 +559,15 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -1,93 +1,75 @@
|
||||
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.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
IRateProviderFactory _RateProviders;
|
||||
ContentSecurityPolicies _CSP;
|
||||
RateFetcher _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProviderFactory _FeeProviderFactory;
|
||||
private CurrencyNameTable _CurrencyNameTable;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
public InvoiceController(InvoiceRepository invoiceRepository,
|
||||
private readonly BTCPayWalletProvider _WalletProvider;
|
||||
IServiceProvider _ServiceProvider;
|
||||
public InvoiceController(
|
||||
IServiceProvider serviceProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
IRateProviderFactory rateProviders,
|
||||
RateFetcher rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ExplorerClientProvider explorerClientProviders,
|
||||
IFeeProviderFactory feeProviderFactory)
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ContentSecurityPolicies csp,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ExplorerClients = explorerClientProviders;
|
||||
_ServiceProvider = serviceProvider;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_UserManager = userManager;
|
||||
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
_CSP = csp;
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
|
||||
if (derivationStrategies.Count == 0)
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
entity.SetDerivationStrategies(derivationStrategies);
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
@ -102,6 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -112,80 +95,167 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
var queries = derivationStrategies
|
||||
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
|
||||
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
|
||||
Network: derivationStrategy.Network,
|
||||
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network),
|
||||
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
|
||||
.Where(_ => _.Wallet != null &&
|
||||
_.FeeRateProvider != null &&
|
||||
_.RateProvider != null)
|
||||
.Select(_ =>
|
||||
{
|
||||
return new
|
||||
{
|
||||
network = _.Network,
|
||||
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
|
||||
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
|
||||
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
|
||||
};
|
||||
});
|
||||
|
||||
bool legacyBTCisSet = false;
|
||||
var cryptoDatas = new Dictionary<string, CryptoData>();
|
||||
foreach (var q in queries)
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
CryptoData cryptoData = new CryptoData();
|
||||
cryptoData.CryptoCode = q.network.CryptoCode;
|
||||
cryptoData.FeeRate = (await q.getFeeRate);
|
||||
cryptoData.TxFee = GetTxFee(storeBlob, cryptoData.FeeRate); // assume price for 100 bytes
|
||||
cryptoData.Rate = await q.getRate;
|
||||
cryptoData.DepositAddress = (await q.getAddress).ToString();
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (q.network.IsBTC)
|
||||
{
|
||||
legacyBTCisSet = true;
|
||||
entity.TxFee = cryptoData.TxFee;
|
||||
entity.Rate = cryptoData.Rate;
|
||||
entity.DepositAddress = cryptoData.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
if (storeBlob.OnChainMinValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency));
|
||||
}
|
||||
|
||||
if (!legacyBTCisSet)
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
#pragma warning disable CS0618
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
|
||||
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc));
|
||||
if (feeProvider != null && rateProvider != null)
|
||||
{
|
||||
var gettingFee = feeProvider.GetFeeRateAsync();
|
||||
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
|
||||
entity.Rate = await gettingRate;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
continue;
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
|
||||
entity.SetCryptoData(cryptoDatas);
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach (var error in logs.ToList())
|
||||
{
|
||||
errors.AppendLine(error.ToString());
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
string errorMessage = null;
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
compare = (a, b) => a > b;
|
||||
limitValue = storeBlob.LightningMaxValue;
|
||||
errorMessage = "The amount of the invoice is too high to be paid with lightning";
|
||||
}
|
||||
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
|
||||
storeBlob.OnChainMinValue != null)
|
||||
{
|
||||
compare = (a, b) => a < b;
|
||||
limitValue = storeBlob.OnChainMinValue;
|
||||
errorMessage = "The amount of the invoice is too low to be paid on chain";
|
||||
}
|
||||
|
||||
if (compare != null)
|
||||
{
|
||||
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
|
||||
if (limitValueRate.BidAsk != null)
|
||||
{
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
@ -194,6 +264,7 @@ namespace BTCPayServer.Controllers
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||
@ -216,11 +287,6 @@ namespace BTCPayServer.Controllers
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
|
@ -20,10 +20,12 @@ using NBitcoin;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
{
|
||||
@ -367,7 +369,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
@ -386,7 +388,7 @@ namespace BTCPayServer.Controllers
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
@ -434,7 +436,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
@ -524,7 +526,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
|
@ -1,40 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
62
BTCPayServer/Controllers/PublicController.cs
Normal file
62
BTCPayServer/Controllers/PublicController.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PublicController : Controller
|
||||
{
|
||||
public PublicController(InvoiceController invoiceController,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
private InvoiceController _InvoiceController;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
ModelState.AddModelError("Store", "Invalid store");
|
||||
else
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (!storeBlob.AnyoneCanInvoice)
|
||||
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
|
||||
}
|
||||
|
||||
if (model == null || model.Price <= 0)
|
||||
ModelState.AddModelError("Price", "Price must be greater than 0");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
BuyerEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,17 +8,19 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
RateFetcher _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
@ -29,46 +31,179 @@ namespace BTCPayServer.Controllers
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
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 = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
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 cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
if (storeId != null)
|
||||
if (storeId == null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if (store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
result.StatusCode = 404;
|
||||
return result;
|
||||
}
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
|
||||
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
|
||||
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
|
||||
foreach (var currency in currencyPairs.Split(','))
|
||||
{
|
||||
if (!CurrencyPair.TryParse(currency, out var pair))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
.Where(r => r.Value.HasValue)
|
||||
.Select(r =>
|
||||
new Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
CurrencyPair = r.Pair.ToString(),
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
|
||||
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
|
||||
{
|
||||
StringBuilder currencyPairsBuilder = new StringBuilder();
|
||||
bool first = true;
|
||||
foreach (var currencyCode in currencyCodes)
|
||||
{
|
||||
if(!first)
|
||||
currencyPairsBuilder.Append(",");
|
||||
first = false;
|
||||
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
|
||||
}
|
||||
return currencyPairsBuilder.ToString();
|
||||
}
|
||||
|
||||
public class Rate
|
||||
{
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "cryptoCode")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currencyPair")]
|
||||
public string CurrencyPair
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,129 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Roles = Roles.ServerAdmin)]
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager, SettingsRepository settingsRepository)
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_Options = options;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
public async Task<IActionResult> Rates()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
|
||||
var vm = new RatesViewModel()
|
||||
{
|
||||
CacheMinutes = rates.CacheInMinutes,
|
||||
PrivateKey = rates.PrivateKey,
|
||||
PublicKey = rates.PublicKey
|
||||
};
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static async Task FetchRateLimits(RatesViewModel vm)
|
||||
{
|
||||
var coinAverage = GetCoinaverageService(vm, false);
|
||||
if (coinAverage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Rates(RatesViewModel vm)
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
rates.PrivateKey = vm.PrivateKey;
|
||||
rates.PublicKey = vm.PublicKey;
|
||||
rates.CacheInMinutes = vm.CacheMinutes;
|
||||
try
|
||||
{
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if (service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
await _SettingsRepository.UpdateSetting(rates);
|
||||
StatusMessage = "Rate settings successfully updated";
|
||||
return RedirectToAction(nameof(Rates));
|
||||
}
|
||||
|
||||
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
|
||||
{
|
||||
var settings = new CoinAverageSettings()
|
||||
{
|
||||
KeyPair = (vm.PublicKey, vm.PrivateKey)
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
@ -43,6 +141,224 @@ namespace BTCPayServer.Controllers
|
||||
return View(users);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.Email = user.Email;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
public IActionResult Maintenance()
|
||||
{
|
||||
MaintenanceViewModel vm = new MaintenanceViewModel();
|
||||
vm.UserName = "btcpayserver";
|
||||
vm.DNSDomain = this.Request.Host.Host;
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
vm.DNSDomain = null;
|
||||
return View(vm);
|
||||
}
|
||||
[Route("server/maintenance")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
vm.SetConfiguredSSH(_Options.SSHSettings);
|
||||
if (command == "changedomain")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
|
||||
return View(vm);
|
||||
}
|
||||
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
|
||||
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
|
||||
return View(vm);
|
||||
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
|
||||
return View(vm);
|
||||
}
|
||||
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
|
||||
return View(vm);
|
||||
}
|
||||
var builder = new UriBuilder();
|
||||
using (var client = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
}))
|
||||
{
|
||||
try
|
||||
{
|
||||
builder.Scheme = this.Request.Scheme;
|
||||
builder.Host = vm.DNSDomain;
|
||||
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
|
||||
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
|
||||
await Task.WhenAll(addresses1, addresses2);
|
||||
|
||||
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
|
||||
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
|
||||
if (!hasCommonAddress)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
messages.Add(ex.Message);
|
||||
if (ex.InnerException != null)
|
||||
messages.Add(ex.InnerException.Message);
|
||||
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var error = RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
|
||||
if (error != null)
|
||||
return error;
|
||||
|
||||
builder.Path = null;
|
||||
builder.Query = null;
|
||||
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-update.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return RedirectToAction(nameof(Maintenance));
|
||||
}
|
||||
|
||||
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
|
||||
[HttpGet]
|
||||
[Route("runid")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult SeeRunId(string expected = null)
|
||||
{
|
||||
if (expected == RunId)
|
||||
return Ok();
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
|
||||
{
|
||||
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
|
||||
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
|
||||
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
|
||||
|
||||
if (_Options.TrustedFingerprints.Count != 0)
|
||||
{
|
||||
sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
|
||||
{
|
||||
if (_Options.TrustedFingerprints.Count == 0)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
|
||||
}
|
||||
else
|
||||
{
|
||||
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
|
||||
if(!e.CanTrust)
|
||||
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
sshClient.Connect();
|
||||
}
|
||||
catch (Renci.SshNet.Common.SshAuthenticationException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
|
||||
{
|
||||
message = aggrEx.InnerException.Message;
|
||||
}
|
||||
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
|
||||
sshClient.Dispose();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var sshCommand = sshClient.CreateCommand(ssh);
|
||||
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
|
||||
sshCommand.BeginExecute(ar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation("Running SSH command: " + ssh);
|
||||
var result = sshCommand.EndExecute(ar);
|
||||
Logs.PayServer.LogInformation("SSH command executed: " + result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
|
||||
}
|
||||
sshClient.Dispose();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var isAdmin = IsAdmin(roles);
|
||||
bool updated = false;
|
||||
|
||||
if (isAdmin != viewModel.IsAdmin)
|
||||
{
|
||||
if (viewModel.IsAdmin)
|
||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
else
|
||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||
updated = true;
|
||||
}
|
||||
if (updated)
|
||||
{
|
||||
viewModel.StatusMessage = "User successfully updated";
|
||||
}
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
@ -66,6 +382,7 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
await _StoreRepository.CleanUnreachableStores();
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
@ -94,7 +411,166 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies upadated successfully";
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public IActionResult Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
{
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "gRPC",
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
result.HasSSH = _Options.SSHSettings != null;
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LNDGRPCServicesViewModel();
|
||||
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
if (external.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
}
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetLNDConfig(uint configKey)
|
||||
{
|
||||
var conf = _LnConfigProvider.GetConfig(configKey);
|
||||
if (conf == null)
|
||||
return NotFound();
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
[HttpPost]
|
||||
public IActionResult LNDGRPCServicesPOST(string cryptoCode, int index)
|
||||
{
|
||||
var external = GetExternalLNDConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.ChainType = _Options.NetworkType.ToString();
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLNDConnectionString(string cryptoCode, int index)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLNDGRPC>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logging.Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
public IActionResult SSHService(bool downloadKeyFile = false)
|
||||
{
|
||||
var settings = _Options.SSHSettings;
|
||||
if (settings == null)
|
||||
return NotFound();
|
||||
if (downloadKeyFile)
|
||||
{
|
||||
if (!System.IO.File.Exists(settings.KeyFile))
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/theme")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
@ -104,10 +580,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (command == "Test")
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
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";
|
||||
@ -118,11 +597,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
else // if(command == "Save")
|
||||
{
|
||||
ModelState.Remove(nameof(model.TestEmail));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
||||
model.StatusMessage = "Email settings saved";
|
||||
return View(model);
|
||||
|
175
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
175
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
{
|
||||
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(c => c.DerivationStrategyBase.ToString())
|
||||
.FirstOrDefault();
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
|
||||
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
183
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
183
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
|
||||
}
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
|
||||
{
|
||||
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
|
||||
{
|
||||
return CanUseInternalLightning() ? connectionString : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if(connectionString.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.BaseUri?.DnsSafeHost;
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if(connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if(!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
case "test" when paymentMethod == null:
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
case "test":
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning()
|
||||
{
|
||||
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
121
BTCPayServer/Controllers/UserStoresController.cs
Normal file
121
BTCPayServer/Controllers/UserStoresController.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
public UserStoresController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_Repo = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public IActionResult CreateStore()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public string CreatedStoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
StatusMessage = "Store removed successfully";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
487
BTCPayServer/Controllers/WalletsController.cs
Normal file
487
BTCPayServer/Controllers/WalletsController.cs
Normal file
@ -0,0 +1,487 @@
|
||||
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 Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("wallets")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class WalletsController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||
private readonly NBXplorerDashboard _dashboard;
|
||||
private readonly ExplorerClientProvider _explorerProvider;
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
RateFetcher _RateProvider;
|
||||
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;
|
||||
_Repo = repo;
|
||||
_RateProvider = rateProvider;
|
||||
_NetworkProvider = networkProvider;
|
||||
_userManager = userManager;
|
||||
_mvcJsonOptions = mvcJsonOptions;
|
||||
_dashboard = dashboard;
|
||||
_explorerProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ListWallets()
|
||||
{
|
||||
var wallets = new ListWalletsViewModel();
|
||||
var stores = await _Repo.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 _Repo.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();
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var storeData = store.GetStoreBlob();
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateRules.Spread = 0.0m;
|
||||
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
|
||||
model.CryptoCurrency = walletId.CryptoCode;
|
||||
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await _RateProvider.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; }
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
|
||||
DerivationStrategyBase derivationScheme = null,
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
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(derivationScheme);
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
|
||||
var changeAddress = await change;
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
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(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 int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
44
BTCPayServer/CurrencyValue.cs
Normal file
44
BTCPayServer/CurrencyValue.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class CurrencyValue
|
||||
{
|
||||
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
|
||||
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
|
||||
public static bool TryParse(string str, out CurrencyValue value)
|
||||
{
|
||||
value = null;
|
||||
var match = _Regex.Match(str);
|
||||
if (!match.Success ||
|
||||
!decimal.TryParse(match.Groups[1].Value, out var v))
|
||||
return false;
|
||||
|
||||
var currency = match.Groups.Last().Value.ToUpperInvariant();
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
v = Math.Round(v, currencyData.Divisibility);
|
||||
value = new CurrencyValue()
|
||||
{
|
||||
Value = v,
|
||||
Currency = currency
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public decimal Value { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Data/APIKeyData.cs
Normal file
25
BTCPayServer/Data/APIKeyData.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class APIKeyData
|
||||
{
|
||||
[MaxLength(50)]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -20,28 +22,30 @@ namespace BTCPayServer.Data
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public ScriptId GetHash()
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return new ScriptId(Address);
|
||||
return new ScriptId(Address.Substring(0, index));
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public AddressInvoiceData SetHash(ScriptId scriptId, string cryptoCode)
|
||||
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
Address = scriptId + "#" + cryptoCode;
|
||||
Address = address + "#" + paymentMethodId.ToString();
|
||||
return this;
|
||||
}
|
||||
public string GetCryptoCode()
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
|
||||
if (index == -1)
|
||||
return "BTC";
|
||||
return Address.Substring(index + 1);
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.Parse(Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
|
40
BTCPayServer/Data/AppData.cs
Normal file
40
BTCPayServer/Data/AppData.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class AppData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string AppType { get; set; }
|
||||
public StoreData StoreData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset Created
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Settings { get; set; }
|
||||
|
||||
public T GetSettings<T>() where T : class, new()
|
||||
{
|
||||
if (String.IsNullOrEmpty(Settings))
|
||||
return new T();
|
||||
return JsonConvert.DeserializeObject<T>(Settings);
|
||||
}
|
||||
|
||||
public void SetSettings(object value)
|
||||
{
|
||||
Settings = value == null ? null : JsonConvert.SerializeObject(value);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<AppData> Apps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<InvoiceEventData> InvoiceEvents
|
||||
{
|
||||
get; set;
|
||||
@ -81,6 +86,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -92,14 +102,27 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.Entity<InvoiceData>()
|
||||
.HasIndex(o => o.StoreDataId);
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<UserStore>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
@ -107,6 +130,19 @@ namespace BTCPayServer.Data
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.APIKeys)
|
||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(a => a.StoreData);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(pt => pt.ApplicationUser)
|
||||
.WithMany(p => p.UserStores)
|
||||
@ -117,6 +153,10 @@ namespace BTCPayServer.Data
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
@ -125,12 +165,24 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PairingCodeData>()
|
||||
.HasKey(o => o.Id);
|
||||
|
||||
builder.Entity<PendingInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(o => o.PendingInvoices)
|
||||
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
|
||||
builder.Entity<PairedSINData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PairedSINData>(b =>
|
||||
{
|
||||
b.HasIndex(o => o.SIN);
|
||||
b.HasIndex(o => o.StoreDataId);
|
||||
});
|
||||
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
@ -140,6 +192,10 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
|
@ -6,6 +6,11 @@ using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -24,6 +29,15 @@ namespace BTCPayServer.Data
|
||||
_Type = type;
|
||||
}
|
||||
|
||||
|
||||
public DatabaseType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Type;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
@ -31,20 +45,66 @@ namespace BTCPayServer.Data
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
|
||||
{
|
||||
builder
|
||||
.Append("CREATE DATABASE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
|
||||
|
||||
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
|
||||
builder
|
||||
.Append(" TEMPLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
|
||||
|
||||
builder
|
||||
.Append(" LC_CTYPE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" LC_COLLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" ENCODING ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
|
||||
|
||||
if (operation.Tablespace != null)
|
||||
{
|
||||
builder
|
||||
.Append(" TABLESPACE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
|
||||
}
|
||||
|
||||
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
|
||||
|
||||
EndStatement(builder, suppressTransaction: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfigureBuilder(DbContextOptionsBuilder builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseSqlite(_ConnectionString);
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UseNpgsql(_ConnectionString);
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseMemoryStorage(); //Sql provider does not support multiple workers
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
@ -27,15 +32,16 @@ namespace BTCPayServer.Data
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetCryptoCode()
|
||||
public Payments.PaymentMethodId GetPaymentMethodId()
|
||||
{
|
||||
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
|
||||
return string.IsNullOrEmpty(CryptoCode) ? new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)
|
||||
: Payments.PaymentMethodId.Parse(CryptoCode);
|
||||
}
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
|
@ -80,5 +80,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
|
@ -21,6 +21,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -11,5 +11,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,13 @@ using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -27,6 +34,12 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<AppData> Apps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
@ -41,10 +54,12 @@ namespace BTCPayServer.Data
|
||||
set;
|
||||
}
|
||||
|
||||
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
|
||||
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
bool btcReturned = false;
|
||||
|
||||
// Legacy stuff which should go away
|
||||
if (!string.IsNullOrEmpty(DerivationStrategy))
|
||||
{
|
||||
if (networks.BTC != null)
|
||||
@ -60,54 +75,63 @@ namespace BTCPayServer.Data
|
||||
JObject strategies = JObject.Parse(DerivationStrategies);
|
||||
foreach (var strat in strategies.Properties())
|
||||
{
|
||||
var network = networks.GetNetwork(strat.Name);
|
||||
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
|
||||
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network != null)
|
||||
{
|
||||
if (network == networks.BTC && btcReturned)
|
||||
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
|
||||
continue;
|
||||
if (strat.Value.Type == JTokenType.Null)
|
||||
continue;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
|
||||
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme)
|
||||
/// <summary>
|
||||
/// Set or remove a new supported payment method for the store
|
||||
/// </summary>
|
||||
/// <param name="paymentMethodId">The paymentMethodId</param>
|
||||
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
|
||||
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
throw new InvalidOperationException("Argument mismatch");
|
||||
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
bool existing = false;
|
||||
foreach (var strat in strategies.Properties().ToList())
|
||||
{
|
||||
if (strat.Name == network.CryptoCode)
|
||||
var stratId = PaymentMethodId.Parse(strat.Name);
|
||||
if (stratId.IsBTCOnChain)
|
||||
{
|
||||
if (network.IsBTC)
|
||||
DerivationStrategy = null;
|
||||
if (string.IsNullOrEmpty(derivationScheme))
|
||||
// Legacy stuff which should go away
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
if (stratId == paymentMethodId)
|
||||
{
|
||||
if (supportedPaymentMethod == null)
|
||||
{
|
||||
strat.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
strat.Value = new JValue(derivationScheme);
|
||||
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
|
||||
}
|
||||
existing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing && string.IsNullOrEmpty(derivationScheme))
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
{
|
||||
if (network.IsBTC)
|
||||
DerivationStrategy = null;
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
else if (!existing)
|
||||
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
|
||||
// This is deprecated so we don't have to set anymore
|
||||
//if (network.IsBTC)
|
||||
// DerivationStrategy = derivationScheme;
|
||||
else if (!existing && supportedPaymentMethod != null)
|
||||
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
|
||||
DerivationStrategies = strategies.ToString();
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
@ -133,10 +157,36 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
[Obsolete]
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
|
||||
}
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
public bool HasClaim(string claim)
|
||||
{
|
||||
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
|
||||
}
|
||||
|
||||
public byte[] StoreBlob
|
||||
{
|
||||
get;
|
||||
@ -144,11 +194,16 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto 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 string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
|
||||
{
|
||||
return DefaultCrypto ?? "BTC";
|
||||
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
|
||||
}
|
||||
public void SetDefaultCrypto(string defaultCryptoCurrency)
|
||||
{
|
||||
@ -160,7 +215,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public StoreBlob GetStoreBlob()
|
||||
{
|
||||
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool SetStoreBlob(StoreBlob storeBlob)
|
||||
@ -174,9 +232,9 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
public class RateRule_Obsolete
|
||||
{
|
||||
public RateRule()
|
||||
public RateRule_Obsolete()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
@ -196,11 +254,21 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
@ -217,50 +285,114 @@ namespace BTCPayServer.Data
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule>();
|
||||
RateRules.Add(new RateRule() { 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;
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
[Obsolete]
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomCSS { get; set; }
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
public bool RateScripting { get; set; }
|
||||
|
||||
public string RateScript { get; set; }
|
||||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
public string LightningDescriptionTemplate
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
get
|
||||
{
|
||||
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
|
||||
if (rateProvider is CachedRateProvider cachedRateProvider)
|
||||
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
|
||||
}
|
||||
set
|
||||
{
|
||||
_LightningDescriptionTemplate = value;
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(0)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (!RateScripting ||
|
||||
string.IsNullOrEmpty(RateScript) ||
|
||||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
|
||||
{
|
||||
return GetDefaultRateRules(networkProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
||||
public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var network in networkProvider.GetAll())
|
||||
{
|
||||
if (network.DefaultRateRules.Length != 0)
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
rateProvider
|
||||
});
|
||||
builder.AppendLine($"// Default rate rules for {network.CryptoCode}");
|
||||
foreach (var line in network.DefaultRateRules)
|
||||
{
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
builder.AppendLine($"////////");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rateProvider;
|
||||
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
|
||||
|
||||
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange;
|
||||
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
|
||||
|
||||
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
|
||||
rules.Spread = Spread;
|
||||
return rules;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
181
BTCPayServer/DerivationSchemeParser.cs
Normal file
181
BTCPayServer/DerivationSchemeParser.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
|
||||
HashSet<string> hintedLabels = new HashSet<string>();
|
||||
|
||||
var hintDestination = HintScriptPubKey?.GetDestination();
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is KeyId)
|
||||
{
|
||||
hintedLabels.Add("legacy");
|
||||
}
|
||||
if (hintDestination is ScriptId)
|
||||
{
|
||||
hintedLabels.Add("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
if(!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
|
||||
try
|
||||
{
|
||||
var result = new DerivationStrategyFactory(Network).Parse(str);
|
||||
return FindMatch(hintedLabels, result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (IsLabel(parts[i]))
|
||||
{
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is WitKeyId)
|
||||
{
|
||||
hintedLabels.Remove("legacy");
|
||||
hintedLabels.Remove("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
str = string.Join('-', parts.Where(p => !IsLabel(p)));
|
||||
foreach (var label in hintedLabels)
|
||||
{
|
||||
str = $"{str}-[{label}]";
|
||||
}
|
||||
|
||||
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
|
||||
}
|
||||
|
||||
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
|
||||
{
|
||||
var facto = new DerivationStrategyFactory(Network);
|
||||
var firstKeyPath = new KeyPath("0/0");
|
||||
if (HintScriptPubKey == null)
|
||||
return result;
|
||||
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
|
||||
return result;
|
||||
|
||||
if (result is MultisigDerivationStrategy)
|
||||
hintLabels.Add("keeporder");
|
||||
|
||||
var resultNoLabels = result.ToString();
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
throw new FormatException("Could not find any match");
|
||||
}
|
||||
|
||||
private static bool IsLabel(string v)
|
||||
{
|
||||
return v.StartsWith('[') && v.EndsWith(']');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
||||
if (minimumItems == 0) // Optimize default case
|
||||
listOfLists.Add(new List<T>());
|
||||
|
||||
for (int i = 1; i <= nonEmptyCombinations; i++)
|
||||
{
|
||||
List<T> thisCombination = new List<T>(inputList.Count);
|
||||
for (int j = 0; j < inputList.Count; j++)
|
||||
{
|
||||
if ((i >> j & 1) == 1)
|
||||
thisCombination.Add(inputList[j]);
|
||||
}
|
||||
|
||||
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
|
||||
listOfLists.Add(thisCombination);
|
||||
}
|
||||
|
||||
return listOfLists;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,17 +2,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy
|
||||
public class DerivationStrategy : ISupportedPaymentMethod
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
@ -30,7 +32,9 @@ 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);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class AllChannelResponse
|
||||
{
|
||||
public string ShortChannelId { get; set; }
|
||||
public string NodeId1 { get; set; }
|
||||
public string NodeId2 { get; set; }
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class ChannelResponse
|
||||
{
|
||||
|
||||
public string NodeId { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public string State { get; set; }
|
||||
}
|
||||
public static class ChannelStates
|
||||
{
|
||||
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
|
||||
|
||||
public const string NORMAL = "NORMAL";
|
||||
}
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBitcoin.RPC;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class EclairRPCClient
|
||||
{
|
||||
public EclairRPCClient(Uri address, Network network)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
Address = address;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Network Network { get; private set; }
|
||||
|
||||
|
||||
public GetInfoResponse GetInfo()
|
||||
{
|
||||
return GetInfoAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GetInfoResponse> GetInfoAsync()
|
||||
{
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
|
||||
}
|
||||
|
||||
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
|
||||
{
|
||||
var response = await SendCommandAsync(request, throwIfRPCError);
|
||||
return Serializer.ToObject<T>(response.ResultString, Network);
|
||||
}
|
||||
|
||||
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
|
||||
{
|
||||
RPCResponse response = null;
|
||||
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
|
||||
if (response == null)
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
request.WriteJSON(writer);
|
||||
writer.Flush();
|
||||
var json = writer.ToString();
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
#if !(PORTABLE || NETCORE)
|
||||
webRequest.ContentLength = bytes.Length;
|
||||
#endif
|
||||
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
|
||||
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await dataStream.FlushAsync().ConfigureAwait(false);
|
||||
dataStream.Dispose();
|
||||
}
|
||||
WebResponse webResponse = null;
|
||||
WebResponse errorResponse = null;
|
||||
try
|
||||
{
|
||||
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
|
||||
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
|
||||
|
||||
if (throwIfRPCError)
|
||||
response.ThrowIfError();
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
if (ex.Response == null || ex.Response.ContentLength == 0 ||
|
||||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
|
||||
throw;
|
||||
errorResponse = ex.Response;
|
||||
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
|
||||
if (throwIfRPCError)
|
||||
response.ThrowIfError();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (errorResponse != null)
|
||||
{
|
||||
errorResponse.Dispose();
|
||||
errorResponse = null;
|
||||
}
|
||||
if (webResponse != null)
|
||||
{
|
||||
webResponse.Dispose();
|
||||
webResponse = null;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public AllChannelResponse[] AllChannels()
|
||||
{
|
||||
return AllChannelsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<AllChannelResponse[]> AllChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] Channels()
|
||||
{
|
||||
return ChannelsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string[]> ChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Close(string channelId)
|
||||
{
|
||||
CloseAsync(channelId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task CloseAsync(string channelId)
|
||||
{
|
||||
if (channelId == null)
|
||||
throw new ArgumentNullException(nameof(channelId));
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
|
||||
}
|
||||
catch (RPCException ex) when (ex.Message == "closing already in progress")
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelResponse Channel(string channelId)
|
||||
{
|
||||
return ChannelAsync(channelId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<ChannelResponse> ChannelAsync(string channelId)
|
||||
{
|
||||
if (channelId == null)
|
||||
throw new ArgumentNullException(nameof(channelId));
|
||||
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] AllNodes()
|
||||
{
|
||||
return AllNodesAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string[]> AllNodesAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Uri Address { get; private set; }
|
||||
|
||||
private HttpWebRequest CreateWebRequest()
|
||||
{
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
|
||||
webRequest.ContentType = "application/json";
|
||||
webRequest.Method = "POST";
|
||||
return webRequest;
|
||||
}
|
||||
|
||||
|
||||
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
|
||||
{
|
||||
MemoryStream ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
return ms;
|
||||
}
|
||||
|
||||
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
|
||||
{
|
||||
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public string Connect(NodeInfo node)
|
||||
{
|
||||
return ConnectAsync(node).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> ConnectAsync(NodeInfo node)
|
||||
{
|
||||
if (node == null)
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
|
||||
}
|
||||
|
||||
public string Receive(LightMoney amount, string description = null)
|
||||
{
|
||||
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException(nameof(amount));
|
||||
List<object> args = new List<object>();
|
||||
args.Add(amount.MilliSatoshi);
|
||||
if(description != null)
|
||||
{
|
||||
args.Add(description);
|
||||
}
|
||||
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
|
||||
}
|
||||
|
||||
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
|
||||
{
|
||||
if (fundingSatoshi == null)
|
||||
throw new ArgumentNullException(nameof(fundingSatoshi));
|
||||
if (node == null)
|
||||
throw new ArgumentNullException(nameof(node));
|
||||
pushAmount = pushAmount ?? LightMoney.Zero;
|
||||
|
||||
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
||||
|
||||
return result.ResultString;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class GetInfoResponse
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string Alias { get; set; }
|
||||
public int Port { get; set; }
|
||||
public uint256 ChainHash { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
}
|
@ -1,569 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public enum LightMoneyUnit : ulong
|
||||
{
|
||||
BTC = 100000000000,
|
||||
MilliBTC = 100000000,
|
||||
Bit = 100000,
|
||||
Satoshi = 1000,
|
||||
MilliSatoshi = 1
|
||||
}
|
||||
|
||||
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
|
||||
{
|
||||
|
||||
|
||||
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
|
||||
private const NumberStyles BitcoinStyle =
|
||||
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
|
||||
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <param name="nRet"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryParse(string bitcoin, out LightMoney nRet)
|
||||
{
|
||||
nRet = null;
|
||||
|
||||
decimal value;
|
||||
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
nRet = new LightMoney(value, LightMoneyUnit.BTC);
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a bitcoin amount (Culture Invariant)
|
||||
/// </summary>
|
||||
/// <param name="bitcoin"></param>
|
||||
/// <returns></returns>
|
||||
public static LightMoney Parse(string bitcoin)
|
||||
{
|
||||
LightMoney result;
|
||||
if (TryParse(bitcoin, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
throw new FormatException("Impossible to parse the string in a bitcoin amount");
|
||||
}
|
||||
|
||||
long _MilliSatoshis;
|
||||
public long MilliSatoshi
|
||||
{
|
||||
get
|
||||
{
|
||||
return _MilliSatoshis;
|
||||
}
|
||||
// used as a central point where long.MinValue checking can be enforced
|
||||
private set
|
||||
{
|
||||
CheckLongMinValue(value);
|
||||
_MilliSatoshis = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get absolute value of the instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public LightMoney Abs()
|
||||
{
|
||||
var a = this;
|
||||
if (a < LightMoney.Zero)
|
||||
a = -a;
|
||||
return a;
|
||||
}
|
||||
|
||||
public LightMoney(int satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(uint satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(long satoshis)
|
||||
{
|
||||
MilliSatoshi = satoshis;
|
||||
}
|
||||
|
||||
public LightMoney(ulong satoshis)
|
||||
{
|
||||
// overflow check.
|
||||
// ulong.MaxValue is greater than long.MaxValue
|
||||
checked
|
||||
{
|
||||
MilliSatoshi = (long)satoshis;
|
||||
}
|
||||
}
|
||||
|
||||
public LightMoney(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
// sanity check. Only valid units are allowed
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
checked
|
||||
{
|
||||
var satoshi = amount * (long)unit;
|
||||
MilliSatoshi = (long)satoshi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Split the Money in parts without loss
|
||||
/// </summary>
|
||||
/// <param name="parts">The number of parts (must be more than 0)</param>
|
||||
/// <returns>The splitted money</returns>
|
||||
public IEnumerable<LightMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
|
||||
long remain;
|
||||
long result = DivRem(_MilliSatoshis, parts, out remain);
|
||||
|
||||
for (int i = 0; i < parts; i++)
|
||||
{
|
||||
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
private static long DivRem(long a, long b, out long result)
|
||||
{
|
||||
result = a % b;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
|
||||
{
|
||||
return new LightMoney(amount, unit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToDecimal)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToUnit(LightMoneyUnit unit)
|
||||
{
|
||||
CheckMoneyUnit(unit, "unit");
|
||||
// overflow safe because (long / int) always fit in decimal
|
||||
// decimal operations are checked by default
|
||||
return (decimal)MilliSatoshi / (int)unit;
|
||||
}
|
||||
/// <summary>
|
||||
/// Convert Money to decimal (same as ToUnit)
|
||||
/// </summary>
|
||||
/// <param name="unit"></param>
|
||||
/// <returns></returns>
|
||||
public decimal ToDecimal(LightMoneyUnit unit)
|
||||
{
|
||||
return ToUnit(unit);
|
||||
}
|
||||
|
||||
public static LightMoney Coins(decimal coins)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Bits(decimal bits)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Cents(decimal cents)
|
||||
{
|
||||
// overflow safe.
|
||||
// decimal operations are checked by default
|
||||
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(decimal sats)
|
||||
{
|
||||
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(ulong sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney Satoshis(long sats)
|
||||
{
|
||||
return new LightMoney(sats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(long msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
public static LightMoney MilliSatoshis(ulong msats)
|
||||
{
|
||||
return new LightMoney(msats);
|
||||
}
|
||||
|
||||
#region IEquatable<Money> Members
|
||||
|
||||
public bool Equals(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
public int CompareTo(LightMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return 1;
|
||||
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
LightMoney m = obj as LightMoney;
|
||||
if (m != null)
|
||||
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
|
||||
#if !(PORTABLE || NETCORE)
|
||||
return _MilliSatoshis.CompareTo(obj);
|
||||
#else
|
||||
return _Satoshis.CompareTo((long)obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static LightMoney operator -(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator -(LightMoney left)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(-left._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator +(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(int left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator *(LightMoney right, int left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
|
||||
}
|
||||
public static LightMoney operator *(long left, LightMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
public static LightMoney operator *(LightMoney right, long left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
|
||||
}
|
||||
|
||||
public static LightMoney operator /(LightMoney left, long right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
return new LightMoney(checked(left._MilliSatoshis / right));
|
||||
}
|
||||
|
||||
public static bool operator <(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis < right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis > right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator <=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis <= right._MilliSatoshis;
|
||||
}
|
||||
public static bool operator >=(LightMoney left, LightMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException("left");
|
||||
if (right == null)
|
||||
throw new ArgumentNullException("right");
|
||||
return left._MilliSatoshis >= right._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(long value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
public static implicit operator LightMoney(int value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(uint value)
|
||||
{
|
||||
return new LightMoney(value);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(ulong value)
|
||||
{
|
||||
return new LightMoney(checked((long)value));
|
||||
}
|
||||
|
||||
public static implicit operator long(LightMoney value)
|
||||
{
|
||||
return value.MilliSatoshi;
|
||||
}
|
||||
|
||||
public static implicit operator ulong(LightMoney value)
|
||||
{
|
||||
return checked((ulong)value.MilliSatoshi);
|
||||
}
|
||||
|
||||
public static implicit operator LightMoney(string value)
|
||||
{
|
||||
return LightMoney.Parse(value);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
LightMoney item = obj as LightMoney;
|
||||
if (item == null)
|
||||
return false;
|
||||
return _MilliSatoshis.Equals(item._MilliSatoshis);
|
||||
}
|
||||
public static bool operator ==(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a._MilliSatoshis == b._MilliSatoshis;
|
||||
}
|
||||
|
||||
public static bool operator !=(LightMoney a, LightMoney b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _MilliSatoshis.GetHashCode();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a culture invariant string representation of Bitcoin amount
|
||||
/// </summary>
|
||||
/// <param name="fplus">True if show + for a positive amount</param>
|
||||
/// <param name="trimExcessZero">True if trim excess zeroes</param>
|
||||
/// <returns></returns>
|
||||
public string ToString(bool fplus, bool trimExcessZero = true)
|
||||
{
|
||||
var fmt = string.Format("{{0:{0}{1}B}}",
|
||||
(fplus ? "+" : null),
|
||||
(trimExcessZero ? "2" : "11"));
|
||||
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
|
||||
}
|
||||
|
||||
|
||||
static LightMoney _Zero = new LightMoney(0);
|
||||
public static LightMoney Zero
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Zero;
|
||||
}
|
||||
}
|
||||
|
||||
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
|
||||
{
|
||||
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
|
||||
|
||||
public object GetFormat(Type formatType)
|
||||
{
|
||||
return formatType == typeof(ICustomFormatter) ? this : null;
|
||||
}
|
||||
|
||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
||||
{
|
||||
if (!this.Equals(formatProvider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var i = 0;
|
||||
var plus = format[i] == '+';
|
||||
if (plus)
|
||||
i++;
|
||||
int decPos = 0;
|
||||
if (int.TryParse(format.Substring(i, 1), out decPos))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
var unit = format[i];
|
||||
var unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
switch (unit)
|
||||
{
|
||||
case 'B':
|
||||
unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
break;
|
||||
}
|
||||
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
|
||||
var zeros = new string('0', decPos);
|
||||
var rest = new string('#', 11 - decPos);
|
||||
var fmt = plus && val > 0 ? "+" : string.Empty;
|
||||
|
||||
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
|
||||
return string.Format(CultureInfo.InvariantCulture, fmt, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="dust">more or less amount</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, LightMoney dust)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (dust == null)
|
||||
throw new ArgumentNullException("dust");
|
||||
return (amount - this).Abs() <= dust;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell if amount is almost equal to this instance
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <param name="margin">error margin (between 0 and 1)</param>
|
||||
/// <returns>true if equals, else false</returns>
|
||||
public bool Almost(LightMoney amount, decimal margin)
|
||||
{
|
||||
if (amount == null)
|
||||
throw new ArgumentNullException("amount");
|
||||
if (margin < 0.0m || margin > 1.0m)
|
||||
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
|
||||
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
|
||||
return Almost(amount, dust);
|
||||
}
|
||||
|
||||
public static LightMoney Min(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a <= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
public static LightMoney Max(LightMoney a, LightMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException("a");
|
||||
if (b == null)
|
||||
throw new ArgumentNullException("b");
|
||||
if (a >= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
private static void CheckLongMinValue(long value)
|
||||
{
|
||||
if (value == long.MinValue)
|
||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
||||
}
|
||||
|
||||
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
|
||||
{
|
||||
var typeOfMoneyUnit = typeof(LightMoneyUnit);
|
||||
if (!Enum.IsDefined(typeOfMoneyUnit, value))
|
||||
{
|
||||
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
int IComparable.CompareTo(object obj)
|
||||
{
|
||||
return this.CompareTo(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Eclair
|
||||
{
|
||||
public class NodeInfo
|
||||
{
|
||||
public NodeInfo(string nodeId, string host, int port)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
if (nodeId == null)
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
Port = port;
|
||||
Host = host;
|
||||
NodeId = nodeId;
|
||||
}
|
||||
public string NodeId { get; private set; }
|
||||
public string Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
}
|
||||
}
|
@ -88,7 +88,9 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
Logs.Events.LogInformation(evt.ToString());
|
||||
var log = evt.ToString();
|
||||
if(!String.IsNullOrEmpty(log))
|
||||
Logs.Events.LogInformation(log);
|
||||
foreach (var sub in actionList)
|
||||
{
|
||||
try
|
||||
|
@ -2,11 +2,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceDataChangedEvent
|
||||
{
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
|
@ -8,24 +8,20 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
24
BTCPayServer/Events/InvoiceNeedUpdateEvent.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNeedUpdateEvent
|
||||
{
|
||||
public InvoiceNeedUpdateEvent(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
InvoiceId = invoiceId;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNewAddressEvent
|
||||
{
|
||||
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
|
||||
{
|
||||
Address = address;
|
||||
InvoiceId = invoiceId;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public string Address { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,10 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStopWatchedEvent
|
||||
{
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -6,21 +6,6 @@ using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NBXplorerErrorEvent
|
||||
{
|
||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
||||
{
|
||||
Message = errorMessage;
|
||||
Network = network;
|
||||
}
|
||||
public string Message { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
||||
}
|
||||
}
|
||||
public class NBXplorerStateChangedEvent
|
||||
{
|
||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class TxOutReceivedEvent
|
||||
{
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public Script ScriptPubKey { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
|
||||
return $"{address} received a transaction ({Network.CryptoCode})";
|
||||
}
|
||||
}
|
||||
}
|
@ -77,7 +77,7 @@ namespace BTCPayServer
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
|
@ -21,20 +21,75 @@ using BTCPayServer.Services.Wallets;
|
||||
using System.IO;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string PrettyPrint(this TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value * 10m;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value / 10m;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
}
|
||||
public static async Task CloseSocket(this WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
public static bool SupportDropForeignKey(this DatabaseFacade facade)
|
||||
{
|
||||
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
@ -48,11 +103,31 @@ namespace BTCPayServer
|
||||
}
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/"))
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
if (resp.HasStarted)
|
||||
return;
|
||||
resp.OnStarting(() =>
|
||||
{
|
||||
SetHeader(resp, name, value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetHeader(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
var existing = resp.Headers[name].FirstOrDefault();
|
||||
if (existing != null && value == null)
|
||||
resp.Headers.Remove(name);
|
||||
else
|
||||
resp.Headers[name] = value;
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -62,6 +137,31 @@ 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());
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -71,12 +171,76 @@ namespace BTCPayServer
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
|
||||
public static string GetSIN(this ClaimsPrincipal principal)
|
||||
{
|
||||
if (!(controller.User.Identity is BitIdentity))
|
||||
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static string GetStoreId(this ClaimsPrincipal principal)
|
||||
{
|
||||
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
|
||||
{
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
|
||||
}
|
||||
|
||||
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
hashSet.Add(item);
|
||||
}
|
||||
}
|
||||
public static bool GetIsBitpayAPI(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
|
||||
obj is bool b && b;
|
||||
}
|
||||
|
||||
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
|
||||
{
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
return ((string Signature, String Id, String Authorization))obj;
|
||||
}
|
||||
|
||||
public static StoreData GetStoreData(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
|
||||
}
|
||||
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
|
||||
{
|
||||
ctx.Items["BTCPAY.STOREDATA"] = storeData;
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
@ -85,13 +249,5 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static HtmlString ToJSVariableModel(this object o, string variableName)
|
||||
{
|
||||
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
|
||||
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IContentSecurityPolicy : IFilterMetadata { }
|
||||
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool AutoSelf { get; set; } = true;
|
||||
public bool UnsafeInline { get; set; } = true;
|
||||
public bool FixWebsocket { get; set; } = true;
|
||||
public string FontSrc { get; set; } = null;
|
||||
public string ImgSrc { get; set; } = null;
|
||||
public string DefaultSrc { get; set; }
|
||||
public string StyleSrc { get; set; }
|
||||
public string ScriptSrc { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
|
||||
{
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (policies == null)
|
||||
return;
|
||||
if (DefaultSrc != null)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
|
||||
}
|
||||
if (UnsafeInline)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(FontSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ImgSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(StyleSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
|
||||
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
var url = string.Concat(
|
||||
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
policies.Add(new ConsentSecurityPolicy("connect-src", url));
|
||||
}
|
||||
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
{
|
||||
bool hasSelf = false;
|
||||
foreach (var group in policies.Rules.GroupBy(p => p.Name))
|
||||
{
|
||||
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
|
||||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
hasSelf = true;
|
||||
}
|
||||
if (hasSelf)
|
||||
{
|
||||
foreach (var authorized in policies.Authorized)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
@ -43,9 +65,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
|
||||
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
|
||||
return (hasVersion || hasIdentity) == IsBitpayAPI;
|
||||
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
|
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IReferrerPolicy : IFilterMetadata { }
|
||||
public class ReferrerPolicyAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public ReferrerPolicyAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value { get; set; }
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user