Compare commits
1191 Commits
smalldocke
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5147b5f261 | ||
|
|
6ad4941712 | ||
|
|
98d89f924a | ||
|
|
36e1596b5c | ||
|
|
83e0ab4f68 | ||
|
|
c24b47bb03 | ||
|
|
b9f324b2eb | ||
|
|
e0af3bf493 | ||
|
|
06349f9818 | ||
|
|
c46949ac60 | ||
|
|
5e570a84fb | ||
|
|
060d19f6a8 | ||
|
|
b83178be8d | ||
|
|
f72fc4d321 | ||
|
|
9242d645b6 | ||
|
|
05df9b5037 | ||
|
|
456717935c | ||
|
|
98df6b7fdd | ||
|
|
449568be9a | ||
|
|
abb5fd3a6c | ||
|
|
deb868b2c0 | ||
|
|
f0000ceab6 | ||
|
|
d9cba8b0b5 | ||
|
|
becdc3f47d | ||
|
|
aa462e7af1 | ||
|
|
34d6b0dcf3 | ||
|
|
87037e17e1 | ||
|
|
c806e9022e | ||
|
|
39b1861d7a | ||
|
|
a379506b46 | ||
|
|
68513b4668 | ||
|
|
802945291a | ||
|
|
3756b54138 | ||
|
|
9eeaf71195 | ||
|
|
0e4222a1f2 | ||
|
|
34831b2d85 | ||
|
|
aba8cdc8c6 | ||
|
|
ba76d3b059 | ||
|
|
26b3dddc28 | ||
|
|
1a38ffa497 | ||
|
|
2fb63ae18e | ||
|
|
206fd0017f | ||
|
|
5d78c7034c | ||
|
|
f226238779 | ||
|
|
05d8ed054e | ||
|
|
4ebd7c4c2f | ||
|
|
8670e58923 | ||
|
|
5ea76f64bb | ||
|
|
be086282ae | ||
|
|
58bde3c916 | ||
|
|
8af448f724 | ||
|
|
273333b040 | ||
|
|
d03465c240 | ||
|
|
58e9bba5a2 | ||
|
|
c58c60c9fa | ||
|
|
939a575c51 | ||
|
|
ca97104831 | ||
|
|
a51911f4ce | ||
|
|
e8316d959b | ||
|
|
be51a4af50 | ||
|
|
b7dc5b5c5f | ||
|
|
cdac2c264e | ||
|
|
e494a46de9 | ||
|
|
10ac6d65d8 | ||
|
|
7f84ef983c | ||
|
|
9a784b5cce | ||
|
|
dfe7ca478f | ||
|
|
ea4ac907b6 | ||
|
|
fa8bf56623 | ||
|
|
f4640c8c4f | ||
|
|
fdc8281e88 | ||
|
|
8597067cbe | ||
|
|
ae99da7299 | ||
|
|
4666c13a1b | ||
|
|
3118b71004 | ||
|
|
d61f3db4fa | ||
|
|
10e139c3d1 | ||
|
|
206165736a | ||
|
|
f68c50c4cf | ||
|
|
75cbba9427 | ||
|
|
1916ea7050 | ||
|
|
e43031411d | ||
|
|
7b7cd2314d | ||
|
|
4ce8ef6954 | ||
|
|
80f16ce8bc | ||
|
|
fed67b354f | ||
|
|
da251bdbe0 | ||
|
|
11a3564ab4 | ||
|
|
e3f7933ad1 | ||
|
|
8b9d5fbde7 | ||
|
|
916a52b49e | ||
|
|
2ed4f7850f | ||
|
|
a38ad24023 | ||
|
|
ab27e4b6ce | ||
|
|
e9a235c149 | ||
|
|
03c17c046d | ||
|
|
530a38030e | ||
|
|
9d796828f8 | ||
|
|
eaeeea22a9 | ||
|
|
5170fd92e1 | ||
|
|
e5eaf763e7 | ||
|
|
326d9c5e2b | ||
|
|
8ea14672dd | ||
|
|
4d81c236f5 | ||
|
|
a2afbd951e | ||
|
|
35b449a49f | ||
|
|
5ab3da5031 | ||
|
|
dbfd09e2f9 | ||
|
|
24d24f2f25 | ||
|
|
1eeb0ff3ef | ||
|
|
b31fa111b4 | ||
|
|
fb2691748f | ||
|
|
c64dc124b5 | ||
|
|
97214740c1 | ||
|
|
3e0b2047d3 | ||
|
|
2fa2ca69a0 | ||
|
|
f37ef1c2d5 | ||
|
|
f165b14c52 | ||
|
|
c249b842ef | ||
|
|
531f28817e | ||
|
|
654b103128 | ||
|
|
21122b4a1f | ||
|
|
e547b99a47 | ||
|
|
88f7d8248a | ||
|
|
a201004b42 | ||
|
|
0bf9492e1d | ||
|
|
8275c6effb | ||
|
|
3d746aeff9 | ||
|
|
8f7371dc47 | ||
|
|
048eab8625 | ||
|
|
d4068e7dec | ||
|
|
1352c8dcdf | ||
|
|
e2767a85c5 | ||
|
|
09d56b22be | ||
|
|
1ee089836d | ||
|
|
4c9db11e6c | ||
|
|
dddd825571 | ||
|
|
42c9dda280 | ||
|
|
9af5962b1e | ||
|
|
d5b56d3206 | ||
|
|
ddde4ae301 | ||
|
|
a76965d12c | ||
|
|
7fb40b5a81 | ||
|
|
2e75bdf65e | ||
|
|
ac6e63c20f | ||
|
|
5519caf601 | ||
|
|
1814138225 | ||
|
|
d612420059 | ||
|
|
f3aeea447d | ||
|
|
261a1e73a1 | ||
|
|
7c870bdeaa | ||
|
|
15a07a8b47 | ||
|
|
52258aecd1 | ||
|
|
b672a28c1c | ||
|
|
a33de3bec7 | ||
|
|
f98fa1addb | ||
|
|
b5b90871ae | ||
|
|
e937448b6b | ||
|
|
c84c0fce97 | ||
|
|
4456dd04d0 | ||
|
|
6ba0315de4 | ||
|
|
2c6418c058 | ||
|
|
f978e88b85 | ||
|
|
22b8e0b17a | ||
|
|
2bd46f7820 | ||
|
|
d03b511db6 | ||
|
|
0c9a3569e6 | ||
|
|
f200ea4930 | ||
|
|
dfef4e8c7e | ||
|
|
4bf8e25902 | ||
|
|
019f8c420a | ||
|
|
a2dc680338 | ||
|
|
72a102a8fc | ||
|
|
e7c701f3f7 | ||
|
|
7c710e34b7 | ||
|
|
efc9ae6444 | ||
|
|
b20d1567e0 | ||
|
|
21f5f04f64 | ||
|
|
825d5bc78d | ||
|
|
e83cfdb720 | ||
|
|
96633a3b7d | ||
|
|
900f99545d | ||
|
|
082373501e | ||
|
|
279521c507 | ||
|
|
2000affe7b | ||
|
|
7d70e72d91 | ||
|
|
9b6358221a | ||
|
|
5d4d028b0e | ||
|
|
07b1193237 | ||
|
|
56d2293c30 | ||
|
|
84305c84d8 | ||
|
|
fffa6f0bca | ||
|
|
15eee7ae47 | ||
|
|
a490c24351 | ||
|
|
6eb53ed8d2 | ||
|
|
6ca2560a1a | ||
|
|
f69581f69e | ||
|
|
190eabbc5e | ||
|
|
826dc2d4ac | ||
|
|
95f28ac578 | ||
|
|
8d309e4de5 | ||
|
|
9bcf963a4a | ||
|
|
85f508756a | ||
|
|
5ddba1d816 | ||
|
|
eca963def6 | ||
|
|
cfedff079d | ||
|
|
ceef9651f3 | ||
|
|
1d89e331ed | ||
|
|
7860ebbda4 | ||
|
|
2f855b5cc3 | ||
|
|
8ec608f2bb | ||
|
|
4260cb8e04 | ||
|
|
9b6c0010fb | ||
|
|
a01893d09d | ||
|
|
57048b0d05 | ||
|
|
9a64c246b8 | ||
|
|
fa26cd4fcf | ||
|
|
3b192971f0 | ||
|
|
02e4714bce | ||
|
|
342a16ad07 | ||
|
|
31a1ab4c89 | ||
|
|
5de2a002a0 | ||
|
|
9faab8f97d | ||
|
|
dbef90b927 | ||
|
|
30c1536323 | ||
|
|
ef1f78b9e9 | ||
|
|
d7a9af1d7a | ||
|
|
1ac249db79 | ||
|
|
c547f20d20 | ||
|
|
a9a8f56d73 | ||
|
|
42dbfdb59d | ||
|
|
f8707aca8b | ||
|
|
f76543311d | ||
|
|
8a051016f4 | ||
|
|
506388937e | ||
|
|
600a642ebc | ||
|
|
3cbb864713 | ||
|
|
25a3f579d8 | ||
|
|
a858d56552 | ||
|
|
06ad54141e | ||
|
|
ef84e74bbb | ||
|
|
3579fcd226 | ||
|
|
fbf8787ad0 | ||
|
|
f6e1a5c4df | ||
|
|
e03ef32ab9 | ||
|
|
26f23c0e75 | ||
|
|
299032a210 | ||
|
|
f2b91e3513 | ||
|
|
c7b5a73ea1 | ||
|
|
4385f75d39 | ||
|
|
a9059c1fba | ||
|
|
50532c4a92 | ||
|
|
73e6100a19 | ||
|
|
ebec46553c | ||
|
|
7cd88f88da | ||
|
|
0534c50898 | ||
|
|
70d608bc09 | ||
|
|
d0ad70f6cb | ||
|
|
f35be21e74 | ||
|
|
faf7621545 | ||
|
|
092018cb6f | ||
|
|
c2c4cb7709 | ||
|
|
994dbfe55b | ||
|
|
b1a14c1bd3 | ||
|
|
256f62385b | ||
|
|
bc92de9db4 | ||
|
|
8ada11e7cb | ||
|
|
f9a5cdc999 | ||
|
|
898258d87f | ||
|
|
f07649fe1c | ||
|
|
9dfd5f6adf | ||
|
|
cfb9aa9506 | ||
|
|
d7c2da3f11 | ||
|
|
9998c66492 | ||
|
|
5437af5975 | ||
|
|
a8c5bdc615 | ||
|
|
67edae79d0 | ||
|
|
32b048f0b6 | ||
|
|
ab8c365ae5 | ||
|
|
919a1f68d7 | ||
|
|
d42378ff91 | ||
|
|
b287ea5521 | ||
|
|
91e76440d0 | ||
|
|
64376a41e1 | ||
|
|
369d3579e9 | ||
|
|
795af96df2 | ||
|
|
28db6d419f | ||
|
|
f3d4508e69 | ||
|
|
b64b7abc04 | ||
|
|
64cfb2fc90 | ||
|
|
06aa2b5dcc | ||
|
|
9eeb5c6d82 | ||
|
|
8768d129b6 | ||
|
|
a064c60932 | ||
|
|
f2e9615f9b | ||
|
|
7dd960dfee | ||
|
|
578426da15 | ||
|
|
8d09c57c7d | ||
|
|
db3142b92f | ||
|
|
4a0e16861b | ||
|
|
b73cb2e110 | ||
|
|
ad90a241a6 | ||
|
|
7045800e48 | ||
|
|
b5592e0106 | ||
|
|
4874373783 | ||
|
|
194f16c7a5 | ||
|
|
c7b6885eed | ||
|
|
b0b7e98cb9 | ||
|
|
1c535e62d4 | ||
|
|
bdd32e1147 | ||
|
|
45f9a4adc9 | ||
|
|
f28bb22c7f | ||
|
|
000376564e | ||
|
|
416a0e7bd2 | ||
|
|
1f18ac5bd2 | ||
|
|
8332e05023 | ||
|
|
639ad9ba45 | ||
|
|
8e069c5b9b | ||
|
|
33bfb8b9e7 | ||
|
|
15f6c42245 | ||
|
|
23a8e335e4 | ||
|
|
0801eca1eb | ||
|
|
c6ae43adb2 | ||
|
|
0a09a4ba1e | ||
|
|
ebac7ce987 | ||
|
|
a4456088e8 | ||
|
|
a906570f08 | ||
|
|
6becdca483 | ||
|
|
086fc3b42c | ||
|
|
b9fd115c6c | ||
|
|
844af377e2 | ||
|
|
d17f60913a | ||
|
|
19c57353e6 | ||
|
|
a993e45266 | ||
|
|
4e13f90605 | ||
|
|
8a6f6dee9c | ||
|
|
00df1e03be | ||
|
|
f063979667 | ||
|
|
9f691db055 | ||
|
|
07323f16ea | ||
|
|
1ddf28ee2a | ||
|
|
fd61110a1e | ||
|
|
754278f473 | ||
|
|
af2ee4c09c | ||
|
|
381c90fc0d | ||
|
|
030084cdfa | ||
|
|
38ddc7d211 | ||
|
|
a1205d8f97 | ||
|
|
f381e00023 | ||
|
|
4447eea417 | ||
|
|
2fb8e2630f | ||
|
|
f7649f490a | ||
|
|
916329e4ff | ||
|
|
788e800b44 | ||
|
|
51a300b70d | ||
|
|
848c6c2665 | ||
|
|
9d601ec868 | ||
|
|
2a493b911a | ||
|
|
da8b7b7653 | ||
|
|
5cc4389cda | ||
|
|
b3fcda0cf6 | ||
|
|
7cf5f33443 | ||
|
|
b8a4a38184 | ||
|
|
dda0bac750 | ||
|
|
713ff5e256 | ||
|
|
4d32024b90 | ||
|
|
bc72700847 | ||
|
|
969b73a49e | ||
|
|
cd1f5df749 | ||
|
|
2075f2200f | ||
|
|
05235a6de0 | ||
|
|
5238f1f2c7 | ||
|
|
c8e9d2f135 | ||
|
|
72830a49c4 | ||
|
|
4128f8b068 | ||
|
|
0939c00d13 | ||
|
|
8e060f545d | ||
|
|
bbcb4e53e8 | ||
|
|
af9d7ce7a5 | ||
|
|
0595a87f22 | ||
|
|
a1dead87c3 | ||
|
|
e4700d21db | ||
|
|
52585b3348 | ||
|
|
4aa2cce05c | ||
|
|
37c49b356f | ||
|
|
ce7819bdc3 | ||
|
|
d0bb7cf9ac | ||
|
|
05e443fa38 | ||
|
|
3692c797fc | ||
|
|
ea99e3cc9e | ||
|
|
0b659c82e2 | ||
|
|
f8b4c650ba | ||
|
|
ba274bfeb1 | ||
|
|
afb16b5890 | ||
|
|
9091abff44 | ||
|
|
1fff142f6f | ||
|
|
9a08aef0fb | ||
|
|
229bf48bee | ||
|
|
55fbdcb9cd | ||
|
|
53eb9d08b8 | ||
|
|
9d235e28dd | ||
|
|
9fe95038a4 | ||
|
|
893ea4eb94 | ||
|
|
fea82a5f57 | ||
|
|
f7aaa1ae91 | ||
|
|
bdd741e753 | ||
|
|
79fbbbf523 | ||
|
|
86d00dd152 | ||
|
|
e15c7c3969 | ||
|
|
273fffa950 | ||
|
|
ee8f65b251 | ||
|
|
8707ac15a6 | ||
|
|
cb5877d959 | ||
|
|
813a49ad99 | ||
|
|
acf2db03e2 | ||
|
|
ccac0d6463 | ||
|
|
a03a897a0b | ||
|
|
4f9a0688b6 | ||
|
|
3944aca2c0 | ||
|
|
4a35012713 | ||
|
|
88a437fc60 | ||
|
|
6c708a4c07 | ||
|
|
79c9196f22 | ||
|
|
7d44d4cb74 | ||
|
|
89ada4a172 | ||
|
|
b96763eca4 | ||
|
|
6454ffcc80 | ||
|
|
b42e5dc031 | ||
|
|
de6d25edf7 | ||
|
|
edf3fab86a | ||
|
|
3726a56374 | ||
|
|
2a610db2d3 | ||
|
|
8d993ff85f | ||
|
|
ebb2ef5df7 | ||
|
|
0ecbb03e17 | ||
|
|
70f318feb1 | ||
|
|
abadb08b61 | ||
|
|
8a596e3314 | ||
|
|
310098706d | ||
|
|
a4644e8ba5 | ||
|
|
621954d176 | ||
|
|
350972e4b9 | ||
|
|
b5252c83b5 | ||
|
|
56d89517a9 | ||
|
|
6efc72db0a | ||
|
|
9f61543120 | ||
|
|
358c487a89 | ||
|
|
ecbec0c70b | ||
|
|
4e8b803777 | ||
|
|
ad75936d7c | ||
|
|
8d94a3e05f | ||
|
|
7b62008830 | ||
|
|
74236a314e | ||
|
|
a1fd3e5b82 | ||
|
|
f17b5a4227 | ||
|
|
1ef9af916b | ||
|
|
ea00dd0276 | ||
|
|
ef95dda31d | ||
|
|
94d9a58b12 | ||
|
|
77c2d8ac8e | ||
|
|
ad1baa5bde | ||
|
|
d466b64836 | ||
|
|
2528842acb | ||
|
|
cba0150fc4 | ||
|
|
23acb53e84 | ||
|
|
ee85a6d238 | ||
|
|
a80cce1ee6 | ||
|
|
1e4c4df6fb | ||
|
|
3cc354160b | ||
|
|
65becaf257 | ||
|
|
ecf1af0224 | ||
|
|
4604485ce3 | ||
|
|
7c5d4942b8 | ||
|
|
76345c74c6 | ||
|
|
5283ea3c4a | ||
|
|
e7e1e29336 | ||
|
|
dd01fb7835 | ||
|
|
8c2dbd5808 | ||
|
|
b4237b59ce | ||
|
|
e2bd17b5b7 | ||
|
|
b491a6b10d | ||
|
|
aac4bc7060 | ||
|
|
18471e33af | ||
|
|
2b96fe49f7 | ||
|
|
6250a78430 | ||
|
|
04dfb92d43 | ||
|
|
963f2cb914 | ||
|
|
b84337fccf | ||
|
|
09efceaeea | ||
|
|
853955171e | ||
|
|
9ba550201c | ||
|
|
3c8c0bb1e0 | ||
|
|
998f598e2c | ||
|
|
f0260bf794 | ||
|
|
58cc70b7bc | ||
|
|
9a711cb1aa | ||
|
|
f45e49bc42 | ||
|
|
4c477aeab8 | ||
|
|
5428ed0caa | ||
|
|
f8f58036db | ||
|
|
1527584e08 | ||
|
|
b3e3c673c3 | ||
|
|
7912fafa85 | ||
|
|
6823a7009d | ||
|
|
7c5c43dd14 | ||
|
|
ee9dcbf5d5 | ||
|
|
c9171b4d24 | ||
|
|
2ab475b351 | ||
|
|
16b060f6dc | ||
|
|
da32bfbab1 | ||
|
|
48e9e4a4fd | ||
|
|
93b4e77ca0 | ||
|
|
1726bbcdca | ||
|
|
8bc1c46163 | ||
|
|
eb9b7caed7 | ||
|
|
b215e91876 | ||
|
|
546ee710b5 | ||
|
|
7142581ac0 | ||
|
|
80faf48bfe | ||
|
|
45819a7c47 | ||
|
|
cde241b3f5 | ||
|
|
c4e25d3894 | ||
|
|
4a32c51584 | ||
|
|
7e9fa2cea9 | ||
|
|
4fb78e9cc3 | ||
|
|
4d60e2b62e | ||
|
|
6ae1b8c0bb | ||
|
|
f8859115a9 | ||
|
|
1d68c24277 | ||
|
|
20a0b98146 | ||
|
|
98bbdc3d0e | ||
|
|
0e7199a661 | ||
|
|
592233ac0a | ||
|
|
70b54c1f85 | ||
|
|
e0354f41c3 | ||
|
|
56d2bf2904 | ||
|
|
ce2f21fbc4 | ||
|
|
cc548a4aba | ||
|
|
6675b56132 | ||
|
|
a33be45253 | ||
|
|
a8512a7cea | ||
|
|
e4080a658c | ||
|
|
8b6ec4ae94 | ||
|
|
28e7545be5 | ||
|
|
8576184ded | ||
|
|
b04d4971e2 | ||
|
|
83f117a751 | ||
|
|
0a7d7972f0 | ||
|
|
b9543a1430 | ||
|
|
f7077870a7 | ||
|
|
860a6c47f8 | ||
|
|
1436809365 | ||
|
|
99b072dc75 | ||
|
|
82f53b609c | ||
|
|
7c8be9dbc9 | ||
|
|
49ef619272 | ||
|
|
9ca3665c3d | ||
|
|
4b17554b19 | ||
|
|
2c1cf53048 | ||
|
|
24ad9535d5 | ||
|
|
81a79d864a | ||
|
|
77a458a26e | ||
|
|
d0dabc96c8 | ||
|
|
6228d18fb8 | ||
|
|
80b79e2fa5 | ||
|
|
03999d5e98 | ||
|
|
e4c21e4e8a | ||
|
|
5e3ecee2c0 | ||
|
|
78e87e4a13 | ||
|
|
413533dbdf | ||
|
|
71d5c187ea | ||
|
|
f437a3365f | ||
|
|
8d3bec302d | ||
|
|
76ecdb250b | ||
|
|
7b3b92b3af | ||
|
|
e85dbe5d28 | ||
|
|
97a6ae17b7 | ||
|
|
d526a8ee9a | ||
|
|
4d81308f5b | ||
|
|
a38720cbce | ||
|
|
015f9b5e32 | ||
|
|
9a36db30fd | ||
|
|
b33af19886 | ||
|
|
351b4c7919 | ||
|
|
81b7623e54 | ||
|
|
8715d3d1ba | ||
|
|
7dacd8e892 | ||
|
|
3ae2befd91 | ||
|
|
8d989ef3c3 | ||
|
|
037f753815 | ||
|
|
46828bd735 | ||
|
|
9ffe87a4e3 | ||
|
|
4b1a6e52d5 | ||
|
|
7230e89029 | ||
|
|
9cdfc5ceac | ||
|
|
a8d6180d07 | ||
|
|
713368e6ab | ||
|
|
c24738ac1d | ||
|
|
e59c1c46e7 | ||
|
|
f43919ba8f | ||
|
|
cc1626594c | ||
|
|
70416f5a5d | ||
|
|
faff2699d5 | ||
|
|
9db0bea772 | ||
|
|
db6a5d99be | ||
|
|
6a1bcecd6e | ||
|
|
0e316fba7b | ||
|
|
51b82602ce | ||
|
|
5edc3be0b3 | ||
|
|
c6f3c20c62 | ||
|
|
9a2f6d83a5 | ||
|
|
29d53baa84 | ||
|
|
20b50df1dc | ||
|
|
8635bc9dec | ||
|
|
0447e857f2 | ||
|
|
002aba4a4b | ||
|
|
5f78363479 | ||
|
|
86562f82b9 | ||
|
|
61f7d31ab4 | ||
|
|
15a1701426 | ||
|
|
b6d1935dec | ||
|
|
bc7ad92ddb | ||
|
|
9f20f9263b | ||
|
|
f2de46d24d | ||
|
|
e7224324eb | ||
|
|
555e6c197d | ||
|
|
70005df0b7 | ||
|
|
3fc5dc46cc | ||
|
|
d8293c9a7a | ||
|
|
a739c1b9e1 | ||
|
|
fd91fc8453 | ||
|
|
1d3398bbad | ||
|
|
7f5facdaa5 | ||
|
|
bb5422d6a9 | ||
|
|
c8b8050a6f | ||
|
|
24874195d9 | ||
|
|
72cc6737c0 | ||
|
|
5eaaeab698 | ||
|
|
1ff970e1d3 | ||
|
|
b72819c219 | ||
|
|
836b9a8f57 | ||
|
|
7f82f634db | ||
|
|
cfa0de1a9e | ||
|
|
56365cdae0 | ||
|
|
01c2b8c489 | ||
|
|
3c8a998e83 | ||
|
|
5f2c27b675 | ||
|
|
a4da994c67 | ||
|
|
2e02de3290 | ||
|
|
90e3285c07 | ||
|
|
713f0499d3 | ||
|
|
870eca72c0 | ||
|
|
9530bcee03 | ||
|
|
3025329cab | ||
|
|
6bd8e6f210 | ||
|
|
44a6af04c3 | ||
|
|
74409946ce | ||
|
|
3ba4edb0eb | ||
|
|
fa9b9c1986 | ||
|
|
48bc2203e5 | ||
|
|
7c2231b236 | ||
|
|
1883c10f4a | ||
|
|
9e9b38d78e | ||
|
|
5d8a24898a | ||
|
|
449b7aadcc | ||
|
|
b707ff57e2 | ||
|
|
dca91aa915 | ||
|
|
9084c0dc9c | ||
|
|
d4db7c2b8c | ||
|
|
bfc37f46c4 | ||
|
|
22bfb7c262 | ||
|
|
37bbc48c53 | ||
|
|
d543daea1d | ||
|
|
66d321c031 | ||
|
|
d8fa1d0d84 | ||
|
|
ca7859ad38 | ||
|
|
e7817e44d7 | ||
|
|
1fe37e8568 | ||
|
|
d7bf343fd5 | ||
|
|
f30f486452 | ||
|
|
2e2f689538 | ||
|
|
fbb91b94bb | ||
|
|
e443dad842 | ||
|
|
fd432961fe | ||
|
|
dac34e77e5 | ||
|
|
5763ed7864 | ||
|
|
180c0f06c8 | ||
|
|
9323386829 | ||
|
|
b6f6ec7e3e | ||
|
|
f1337556f0 | ||
|
|
02f88a9a2c | ||
|
|
b98d81687e | ||
|
|
93fb32796b | ||
|
|
2aaad298f4 | ||
|
|
5e68f8566d | ||
|
|
3a1d8cf94d | ||
|
|
ee08904e05 | ||
|
|
45878f611f | ||
|
|
cdc97df92d | ||
|
|
b591e98b66 | ||
|
|
7bedfc291b | ||
|
|
9e0f975c42 | ||
|
|
0e61a0c604 | ||
|
|
1ad62ef2d5 | ||
|
|
e4c8ec545f | ||
|
|
9374cf783d | ||
|
|
d557fb5ce3 | ||
|
|
f32a2fdba0 | ||
|
|
2512ff3ae1 | ||
|
|
1e5224f84a | ||
|
|
4ff911faf1 | ||
|
|
fdaa18e8db | ||
|
|
453addb443 | ||
|
|
5a0e219c6f | ||
|
|
649e82a9f7 | ||
|
|
5ee8b32a56 | ||
|
|
b2833fc92a | ||
|
|
79fdcbb6d8 | ||
|
|
ed7120f740 | ||
|
|
957a4a652a | ||
|
|
06b0d13511 | ||
|
|
3c6cfa49eb | ||
|
|
9c6f3126bb | ||
|
|
ca9fa2117b | ||
|
|
87d9e0e54f | ||
|
|
14d3b874de | ||
|
|
9904cc543e | ||
|
|
235acc7022 | ||
|
|
6f9981f2ad | ||
|
|
96672560a6 | ||
|
|
05fcf2e148 | ||
|
|
e8ec943fba | ||
|
|
240421f026 | ||
|
|
73db6ce427 | ||
|
|
51cd86d860 | ||
|
|
99ca6e4588 | ||
|
|
4ee4bc922c | ||
|
|
3d48872aed | ||
|
|
b3f3e44a8f | ||
|
|
22640cbc75 | ||
|
|
2742e60e53 | ||
|
|
4dd4051b78 | ||
|
|
8ed3832bb5 | ||
|
|
8b7701f326 | ||
|
|
ae65894a14 | ||
|
|
b6c5652659 | ||
|
|
f0ff747ba2 | ||
|
|
6b065d0db6 | ||
|
|
0e55f31b9a | ||
|
|
a1467ae187 | ||
|
|
b84e41864b | ||
|
|
27fcffebd2 | ||
|
|
92242b85bc | ||
|
|
d7dec3824a | ||
|
|
733b41931a | ||
|
|
ee2ba53e44 | ||
|
|
1d7f256c46 | ||
|
|
f80e856561 | ||
|
|
5e37817847 | ||
|
|
18be1f9794 | ||
|
|
0adf9fddd3 | ||
|
|
3005dbe24b | ||
|
|
2a21003eda | ||
|
|
89b8c349b8 | ||
|
|
0233281875 | ||
|
|
d25e372449 | ||
|
|
cc7d8cb431 | ||
|
|
512e3e8192 | ||
|
|
a1d25cb22f | ||
|
|
f2d876ecf4 | ||
|
|
3df70c6ddd | ||
|
|
74bfd27395 | ||
|
|
9c6a8ec5fd | ||
|
|
a1da63daff | ||
|
|
f254523eb8 | ||
|
|
a5c23997e7 | ||
|
|
f6916bb68d | ||
|
|
1c093c3a34 | ||
|
|
30e7b88cd3 | ||
|
|
afd9496539 | ||
|
|
d00d2fa145 | ||
|
|
8f3227e3fa | ||
|
|
8071c44dda | ||
|
|
3a44058c60 | ||
|
|
ae464575bd | ||
|
|
06bc369463 | ||
|
|
6f0fbc5829 | ||
|
|
95fd94e0ed | ||
|
|
fbf09ff233 | ||
|
|
407839333e | ||
|
|
cc2276fb6d | ||
|
|
b77545f127 | ||
|
|
faf36b4dfd | ||
|
|
be5ca52720 | ||
|
|
98afc2bf71 | ||
|
|
5da254b803 | ||
|
|
6069d0a06a | ||
|
|
c97f807c17 | ||
|
|
bbc358d9e4 | ||
|
|
d0145c5946 | ||
|
|
03de0afaf7 | ||
|
|
4c172224bf | ||
|
|
d76dff5731 | ||
|
|
dbd3626bdf | ||
|
|
06d63d34eb | ||
|
|
b24b5c2268 | ||
|
|
59dad6922a | ||
|
|
c36bb8b53b | ||
|
|
6a558d876d | ||
|
|
79b9638162 | ||
|
|
e120fa87e1 | ||
|
|
acaca283ba | ||
|
|
82128d0ab2 | ||
|
|
65beaa6815 | ||
|
|
84ce4130a2 | ||
|
|
23618f1cea | ||
|
|
f5a739f1ba | ||
|
|
e82faf4ab0 | ||
|
|
8bcc16187f | ||
|
|
15324901c2 | ||
|
|
8f3a468f5c | ||
|
|
f9cdcf2386 | ||
|
|
b850fbc487 | ||
|
|
567cd245be | ||
|
|
dd9524af50 | ||
|
|
f6eddf7af2 | ||
|
|
d064d559c8 | ||
|
|
a0085bb9a0 | ||
|
|
b93e26ab80 | ||
|
|
b9f113a415 | ||
|
|
4cea321d87 | ||
|
|
f95464e53f | ||
|
|
95d9107a6f | ||
|
|
7added24c6 | ||
|
|
7e095d0181 | ||
|
|
876069bdb7 | ||
|
|
4fe7a78bce | ||
|
|
06be151fb6 | ||
|
|
5b4c62a30f | ||
|
|
daa667bf0b | ||
|
|
9bd740f8cd | ||
|
|
4a32fcfa10 | ||
|
|
fef3fa241c | ||
|
|
175400f65b | ||
|
|
bfb4874dd7 | ||
|
|
6df39c9f1e | ||
|
|
0be9ffac8c | ||
|
|
3a488725a1 | ||
|
|
45e7f1ab0b | ||
|
|
aacb4b88d9 | ||
|
|
10f9469029 | ||
|
|
37e18a2049 | ||
|
|
2805439b80 | ||
|
|
6a5799dfe2 | ||
|
|
c68a058069 | ||
|
|
1a550a92e6 | ||
|
|
39d1901a80 | ||
|
|
1dc5290396 | ||
|
|
820c4b824b | ||
|
|
302e3b35d0 | ||
|
|
7b20acef8b | ||
|
|
36837b14ce | ||
|
|
a16012d5f7 | ||
|
|
1bc990c067 | ||
|
|
cbf59321ee | ||
|
|
f0c7772ac8 | ||
|
|
5aafdb54d2 | ||
|
|
b60c291207 | ||
|
|
3afe782fff | ||
|
|
d61d002677 | ||
|
|
a0d469651f | ||
|
|
cb5164b73d | ||
|
|
3eef314255 | ||
|
|
a8c526258b | ||
|
|
e267204b75 | ||
|
|
231824ee94 | ||
|
|
6d829dcfe8 | ||
|
|
a135445e2f | ||
|
|
3f2578adfe | ||
|
|
226ea19ca1 | ||
|
|
c1c8bf0047 | ||
|
|
4a03273ef0 | ||
|
|
f12c367030 | ||
|
|
3a8ac1ff64 | ||
|
|
3788e85120 | ||
|
|
b348737fe3 | ||
|
|
51aeb4401c | ||
|
|
89e4f9e154 | ||
|
|
f560a9f381 | ||
|
|
194fa7524c | ||
|
|
e2dbaa3352 | ||
|
|
e9f865fdab | ||
|
|
b5db39282d | ||
|
|
482f5488c8 | ||
|
|
53deaeb13b | ||
|
|
bb37a17b30 | ||
|
|
e934f0bfaf | ||
|
|
34395c397b | ||
|
|
784a39a1cb | ||
|
|
3b4a0684e3 | ||
|
|
2289e76624 | ||
|
|
c7655be34b | ||
|
|
f56a79100d | ||
|
|
5ef7fa1709 | ||
|
|
0437798a95 | ||
|
|
02ddfa88e9 | ||
|
|
c3e8d6023d | ||
|
|
43e483709e | ||
|
|
bc571edcfb | ||
|
|
5a87fbe5cf | ||
|
|
77b2bd06bb | ||
|
|
f1e7bb6b14 | ||
|
|
16df4f1cf5 | ||
|
|
7ec7d98ef2 | ||
|
|
7ed77bb0df | ||
|
|
e52eb8ab60 | ||
|
|
7db8160dbf | ||
|
|
1ef98e7a9c | ||
|
|
cb29f39d4d | ||
|
|
69ba5bc824 | ||
|
|
cfeb95802c | ||
|
|
a7c78c055b | ||
|
|
df1d1350b4 | ||
|
|
590a3f8fb9 | ||
|
|
8ba3e9ce86 | ||
|
|
7472b042fc | ||
|
|
9f76a84bcb | ||
|
|
fc1bd38411 | ||
|
|
f9a39e827c | ||
|
|
a590929340 | ||
|
|
f64926933c | ||
|
|
92a89aa4dc | ||
|
|
6c84739788 | ||
|
|
ea0e372eb7 | ||
|
|
91d3d92cbd | ||
|
|
433ae9afeb | ||
|
|
261fc5932d | ||
|
|
4a89975d0e | ||
|
|
6f952775a1 | ||
|
|
84ae9324c6 | ||
|
|
5d2efb570b | ||
|
|
c9e8527ceb | ||
|
|
07710c2d2d | ||
|
|
0d6ad418e9 | ||
|
|
f6f0c50290 | ||
|
|
913eb95be1 | ||
|
|
eca29c308a | ||
|
|
6accf14da4 | ||
|
|
8506f44e3b | ||
|
|
fb63c9eff0 | ||
|
|
130da35412 | ||
|
|
374dd83881 | ||
|
|
b311287a97 | ||
|
|
984c582507 | ||
|
|
de6245f0c9 | ||
|
|
efe035bd56 | ||
|
|
bf0f89fb5d | ||
|
|
8ec7dbd861 | ||
|
|
8333f00d6d | ||
|
|
a4b4545510 | ||
|
|
5160a155c7 | ||
|
|
4ca4c0e249 | ||
|
|
7bc154623e | ||
|
|
862ba3d53c | ||
|
|
dfaa7adbc3 | ||
|
|
322281e564 | ||
|
|
f1fd9730e6 | ||
|
|
ade0601bbc | ||
|
|
be9d6e76c5 | ||
|
|
2a0c4f1685 | ||
|
|
6c7695d3b6 | ||
|
|
b4a4bfdb25 | ||
|
|
0a3cebb52a | ||
|
|
f4fb9edff7 | ||
|
|
edcc0e0a45 | ||
|
|
b40e4d846a | ||
|
|
17dfb21ff9 | ||
|
|
3479606c97 | ||
|
|
4e03857bc7 | ||
|
|
b05126e1ce | ||
|
|
008630a9a0 | ||
|
|
cc3d29167c | ||
|
|
028b097792 | ||
|
|
efce69acc1 | ||
|
|
edbb68ac6a | ||
|
|
e92b1fcadf | ||
|
|
e61d3461f5 | ||
|
|
bcf72c158b | ||
|
|
445d29df21 | ||
|
|
7adfc16bb0 | ||
|
|
997d34be6d | ||
|
|
b8f716de4d | ||
|
|
6f3c8c7f5d | ||
|
|
b4ef988195 | ||
|
|
760bf2108e | ||
|
|
8fc05f2cf7 | ||
|
|
e3c55eeab4 | ||
|
|
39581b0f6b | ||
|
|
d6bc3c7571 | ||
|
|
4e15094403 | ||
|
|
241770fabd | ||
|
|
db44f0ade2 | ||
|
|
a2fa25d5f4 | ||
|
|
6870359088 | ||
|
|
7d7031b20e | ||
|
|
936a538da5 | ||
|
|
85f1f3205d | ||
|
|
6c464707e8 | ||
|
|
e8483d8e25 | ||
|
|
f45d19ece3 | ||
|
|
b5221f67cd | ||
|
|
6dc498b22f | ||
|
|
beb7c8d4fa | ||
|
|
6984f1ef8e | ||
|
|
e7cc3c9412 | ||
|
|
c0fd730827 | ||
|
|
54d897d7e6 | ||
|
|
a09ac55a99 | ||
|
|
bca43e18b4 | ||
|
|
e72d478e7b | ||
|
|
c29f227833 | ||
|
|
28ef902464 | ||
|
|
7d964eb061 | ||
|
|
9f474d142d | ||
|
|
a4f33f898d | ||
|
|
1497c7e08b | ||
|
|
505e6662ba | ||
|
|
18930c7a0b | ||
|
|
71a0623aed | ||
|
|
88a8db8be3 | ||
|
|
da7df86019 | ||
|
|
9422ea02af | ||
|
|
cc0a9c1bb7 | ||
|
|
c37b21fa0c | ||
|
|
f18ba2f5c6 | ||
|
|
82646bc041 | ||
|
|
a632abf8c3 | ||
|
|
f9a937d4fc | ||
|
|
0b67b7d0d0 | ||
|
|
d3fbc499a9 | ||
|
|
b85d4842d3 | ||
|
|
bde87d7c17 | ||
|
|
244adcb313 | ||
|
|
7a6f44993d | ||
|
|
be5d1b5182 | ||
|
|
b281286a35 | ||
|
|
aebd0d274d | ||
|
|
043356253e | ||
|
|
cb43cd1d35 | ||
|
|
62ae513dd0 | ||
|
|
042b690b45 | ||
|
|
e2c2cc07c0 | ||
|
|
ad9283e849 | ||
|
|
a54227d567 | ||
|
|
73dac5d01f | ||
|
|
c55f65e94d | ||
|
|
ce9f785368 | ||
|
|
34c4c10063 | ||
|
|
b12a4227b1 | ||
|
|
efbd3b823c | ||
|
|
d598f86ff7 | ||
|
|
4ffa5bbfc5 | ||
|
|
6f7f277fc7 | ||
|
|
b64f960401 | ||
|
|
e8fcddcd14 | ||
|
|
5099c87693 | ||
|
|
f55707e509 | ||
|
|
0f3d47ca6a | ||
|
|
8db153b517 | ||
|
|
7be06f21f1 | ||
|
|
c24bc6a376 | ||
|
|
cd42e550a2 | ||
|
|
8cd9bfdbb1 | ||
|
|
6fc5b27c0c | ||
|
|
1b8aadfa82 | ||
|
|
764b1752d7 | ||
|
|
09b3147c6e | ||
|
|
4fa724c88c | ||
|
|
893e58392a | ||
|
|
c3e1c7dd86 | ||
|
|
2c7fa5dc79 | ||
|
|
164f90aedb | ||
|
|
1225375313 | ||
|
|
f96cf0ea6c | ||
|
|
520bd58cde | ||
|
|
85657eefcd | ||
|
|
ed3ce02a74 | ||
|
|
469e2c507e | ||
|
|
66c2f85213 | ||
|
|
2633c4ee4a | ||
|
|
972b16c53f | ||
|
|
a35d34b841 | ||
|
|
e687149636 | ||
|
|
1c70889ebb | ||
|
|
49325079d9 | ||
|
|
9af604834d | ||
|
|
243521c2a9 | ||
|
|
ec02fd91a1 | ||
|
|
fbc1b3e041 | ||
|
|
72511d86de | ||
|
|
e7fe75b982 | ||
|
|
d8ee138522 | ||
|
|
929090d1b1 | ||
|
|
ed662ee1e8 | ||
|
|
3c2489d41e | ||
|
|
900db2780d | ||
|
|
4a7d640f0a | ||
|
|
6ee6b2a4b6 | ||
|
|
916444553a | ||
|
|
67b03fbfc0 | ||
|
|
6a2e3866f2 | ||
|
|
07fe181cbf | ||
|
|
c588da7112 | ||
|
|
8fa607de4d | ||
|
|
c2dce69fe6 | ||
|
|
76c4204dc6 | ||
|
|
e6204c57ec | ||
|
|
0622b5c2fe | ||
|
|
ac905261cb | ||
|
|
a7fecd761f | ||
|
|
4e4a4f01f9 | ||
|
|
35c747030b | ||
|
|
a9b6ef141b | ||
|
|
5b09108a51 | ||
|
|
7d859f9f37 | ||
|
|
14e79c2f8a | ||
|
|
ff67fc2b57 | ||
|
|
f6e118f2e7 | ||
|
|
afd8b0de28 | ||
|
|
09c4ed1853 | ||
|
|
6509624141 | ||
|
|
ab3a914134 | ||
|
|
3965336852 | ||
|
|
c6d8911a9e | ||
|
|
09d5adf7ae | ||
|
|
d7ca0f2dc2 | ||
|
|
0deeb9a08b | ||
|
|
551f48d6bf | ||
|
|
3a0e9344ab | ||
|
|
2a6d728b3a | ||
|
|
08f48100e4 | ||
|
|
d866fc1267 | ||
|
|
81cfcd5cd9 | ||
|
|
88b11589d3 | ||
|
|
cfc74f20a8 | ||
|
|
0037b4a0c0 | ||
|
|
7e20b13e0f | ||
|
|
a8b683a38f | ||
|
|
1860ebb449 | ||
|
|
caf09c205b | ||
|
|
b9019ed83d | ||
|
|
87d3211d78 | ||
|
|
031d578799 | ||
|
|
7196fcfa85 | ||
|
|
994b6c0e7c | ||
|
|
d01eda31d6 | ||
|
|
9babc8364d | ||
|
|
4372c0c654 | ||
|
|
4be7fc4ceb | ||
|
|
460fd5edeb | ||
|
|
c0a112020f | ||
|
|
7b876ff971 | ||
|
|
8e8105797c | ||
|
|
aa7fdf7ff3 | ||
|
|
354d126f57 | ||
|
|
c42ced63a5 | ||
|
|
bc9a31c16e | ||
|
|
e485a18d35 | ||
|
|
d51dbaae49 | ||
|
|
73b826ef34 | ||
|
|
b7e49a2d80 | ||
|
|
15a4644d7e | ||
|
|
1316359c79 | ||
|
|
6748cf66de | ||
|
|
9fd9f6a87c | ||
|
|
09b3c1975d | ||
|
|
0572d770af | ||
|
|
139fa4bd05 | ||
|
|
9280707dfc | ||
|
|
7628b2e4de | ||
|
|
df941a62aa | ||
|
|
670c855dc0 | ||
|
|
4a45f781c6 | ||
|
|
8751027a32 | ||
|
|
ff43044532 | ||
|
|
e3cf900556 | ||
|
|
2c74fc9fcd | ||
|
|
92a2c5a4ad | ||
|
|
ddf608c0c7 | ||
|
|
7847cae40f | ||
|
|
d6c5ae7a31 |
43
.circleci/config.yml
Normal file
43
.circleci/config.yml
Normal file
@ -0,0 +1,43 @@
|
||||
version: 2
|
||||
jobs:
|
||||
test:
|
||||
machine:
|
||||
- image: ubuntu-2004:202201-02
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
cd .circleci && ./run-tests.sh
|
||||
|
||||
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||
|
||||
docker:
|
||||
docker:
|
||||
- image: cimg/base:stable
|
||||
steps:
|
||||
- setup_remote_docker
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
docker buildx create --use
|
||||
docker buildx build -t $DOCKERHUB_REPO:$LATEST_TAG --platform linux/amd64,linux/arm64,linux/arm/v7 --push .
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- test
|
||||
|
||||
publish:
|
||||
jobs:
|
||||
- docker:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# only act on version tags v1.0.0.88 or v1.0.2-1
|
||||
# OR feature tags like abc
|
||||
# OR features on specific versions like v1.0.0.88-abc-1
|
||||
tags:
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
|
||||
7
.circleci/run-tests.sh
Executable file
7
.circleci/run-tests.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
cd ../NBXplorer.Tests
|
||||
docker-compose -v
|
||||
docker-compose build
|
||||
docker-compose run tests
|
||||
@ -121,5 +121,4 @@ bower_components
|
||||
output
|
||||
|
||||
.vs
|
||||
NBXplorer.Tests/
|
||||
**/launchSettings.json
|
||||
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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -26,6 +26,8 @@ bld/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Visual Studio Code directory
|
||||
.vscode/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
@ -46,7 +48,6 @@ dlldata.c
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
@ -286,3 +287,4 @@ __pycache__/
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
/NBXplorer.Tests/Properties/launchSettings.json
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@ -1,19 +1,21 @@
|
||||
FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7 AS builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
|
||||
WORKDIR /source
|
||||
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
|
||||
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
|
||||
# Cache some dependencies
|
||||
RUN cd NBXplorer && dotnet restore && cd ..
|
||||
COPY . .
|
||||
RUN cd NBXplorer && \
|
||||
dotnet add package ILLink.Tasks --version 0.1.5-preview-1461378 --source https://dotnet.myget.org/F/dotnet-core/api/v3/index.json && \
|
||||
dotnet publish --output /app/ --configuration Release -r linux-musl-x64
|
||||
dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM microsoft/dotnet:2.1.0-rc1-runtime-deps-alpine3.7
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0.9-noble
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /datadir
|
||||
ENV NBXPLORER_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
ENTRYPOINT ["./NBXplorer"]
|
||||
ENTRYPOINT ["dotnet", "NBXplorer.dll"]
|
||||
|
||||
12
Examples/MultiSig/MultiSig.csproj
Normal file
12
Examples/MultiSig/MultiSig.csproj
Normal file
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
183
Examples/MultiSig/Program.cs
Normal file
183
Examples/MultiSig/Program.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MultiSig
|
||||
{
|
||||
class Program
|
||||
{
|
||||
class Party
|
||||
{
|
||||
public Party(Mnemonic mnemonic, string password, KeyPath accountKeyPath)
|
||||
{
|
||||
// Note: you could just generate the ExtKey with new ExtKey() and save extKey.GetWif(network) somewhere.
|
||||
// But saving a mnemonic + password is well known UX
|
||||
Mnemonic = mnemonic;
|
||||
PartyName = password; //lazy yes
|
||||
RootExtKey = mnemonic.DeriveExtKey(password);
|
||||
AccountExtPubKey = RootExtKey.Derive(accountKeyPath).Neuter();
|
||||
|
||||
// The AccountKeyPath should be stored along the AccountExtPubKey
|
||||
// This is the keypath + the hash of the root hd key.
|
||||
// During signing, NBitcoin need this information to derive the RootExtKey to the address keypath properly.
|
||||
AccountKeyPath = new RootedKeyPath(RootExtKey.GetPublicKey().GetHDFingerPrint(), accountKeyPath);
|
||||
}
|
||||
public string PartyName;
|
||||
public Mnemonic Mnemonic;
|
||||
public ExtPubKey AccountExtPubKey;
|
||||
public ExtKey RootExtKey;
|
||||
public RootedKeyPath AccountKeyPath;
|
||||
}
|
||||
|
||||
// We will:
|
||||
// 1. Create a multi sig wallet of Alice and Bob
|
||||
// 2. Fund it with 1 BTC
|
||||
// 3. Send 0.4 BTC to a random address from it
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Start bitcoind and NBXplorer in regtest:
|
||||
// * Run "bitcoind -regtest"
|
||||
// * Run ".\build.ps1", then ".\run.ps1 -regtest" in NBXplorer
|
||||
|
||||
var network = Network.RegTest;
|
||||
var client = CreateNBXClient(network);
|
||||
|
||||
// Now let's simulate alice and bob in a 2-2 multisig
|
||||
var alice = new Party(new Mnemonic(Wordlist.English), "Alice",
|
||||
new KeyPath("1'/2'/3'"));
|
||||
var bob = new Party(new Mnemonic(Wordlist.English), "Bob",
|
||||
new KeyPath("5'/2'/3'"));
|
||||
|
||||
|
||||
Console.WriteLine($"Alice should secretly save '{alice.Mnemonic}', and remember her password 'Alice'");
|
||||
Console.WriteLine("---");
|
||||
Console.WriteLine($"Alice should secretly save '{bob.Mnemonic}', and remember her password 'Bob'");
|
||||
|
||||
Console.WriteLine("---");
|
||||
Console.WriteLine($"Alice should share '{alice.AccountExtPubKey.GetWif(network)}' with Bob");
|
||||
Console.WriteLine("---");
|
||||
Console.WriteLine($"Bob should share '{bob.AccountExtPubKey.GetWif(network)}' with Alice");
|
||||
|
||||
var factory = new DerivationStrategyFactory(network);
|
||||
var derivationStrategy = factory.CreateMultiSigDerivationStrategy(new[]
|
||||
{
|
||||
alice.AccountExtPubKey.GetWif(network),
|
||||
bob.AccountExtPubKey.GetWif(network)
|
||||
}, 2, new DerivationStrategyOptions() { ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH });
|
||||
|
||||
Console.WriteLine("---");
|
||||
Console.WriteLine($"The derivation strategy '{derivationStrategy}' represents all the data you need to know to track the multisig wallet");
|
||||
|
||||
// NBXplorer will start tracking this wallet.
|
||||
await client.TrackAsync(derivationStrategy);
|
||||
// This allow you to get events out of NBXPlorer
|
||||
var evts = client.CreateLongPollingNotificationSession();
|
||||
|
||||
// Now let's fund the wallet
|
||||
var address1 = (await client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit)).Address;
|
||||
|
||||
var rpc = new RPCClient(network);
|
||||
// If that fail, your bitcoin node need some bitcoins
|
||||
// bitcoin-cli -regtest getnewaddress
|
||||
// bitcoin-cli -regtest generatetoaddress 101 <address>
|
||||
await rpc.SendToAddressAsync(address1, Money.Coins(1.0m));
|
||||
|
||||
await WaitTransaction(evts, derivationStrategy);
|
||||
Console.WriteLine("---");
|
||||
Console.WriteLine("Sent some money to the multi sig wallet");
|
||||
Console.WriteLine("---");
|
||||
|
||||
// You can list transactions
|
||||
var txs = await client.GetTransactionsAsync(derivationStrategy);
|
||||
Console.WriteLine($"Number of unconf transactions: {txs.UnconfirmedTransactions.Transactions.Count}");
|
||||
Console.WriteLine("---");
|
||||
var balance = await client.GetBalanceAsync(derivationStrategy);
|
||||
Console.WriteLine($"Balance: {balance.Unconfirmed}");
|
||||
|
||||
Console.WriteLine("---");
|
||||
var randomDestination = new Key().PubKey.GetAddress(ScriptPubKeyType.Segwit, network);
|
||||
var psbt = (await client.CreatePSBTAsync(derivationStrategy, new CreatePSBTRequest()
|
||||
{
|
||||
Destinations =
|
||||
{
|
||||
new CreatePSBTDestination()
|
||||
{
|
||||
Destination = randomDestination,
|
||||
Amount = Money.Coins(0.4m),
|
||||
SubstractFees = true // We will pay fee by sending to destination a bit less than 0.4 BTC
|
||||
}
|
||||
},
|
||||
FeePreference = new FeePreference()
|
||||
{
|
||||
// 10 sat/byte. You can remove this in prod, as it will use bitcoin's core estimation.
|
||||
ExplicitFeeRate = new FeeRate(10.0m)
|
||||
}
|
||||
})).PSBT;
|
||||
|
||||
var signedByAlice = Sign(alice, derivationStrategy, psbt);
|
||||
Console.WriteLine("---");
|
||||
var signedByBob = Sign (bob, derivationStrategy, psbt);
|
||||
|
||||
// OK both have signed
|
||||
var fullySignedPSBT = signedByAlice.Combine(signedByBob);
|
||||
fullySignedPSBT.Finalize();
|
||||
var fullySignedTx = fullySignedPSBT.ExtractTransaction();
|
||||
await client.BroadcastAsync(fullySignedTx);
|
||||
// Let's wait NBX receives the tx
|
||||
await WaitTransaction(evts, derivationStrategy);
|
||||
balance = await client.GetBalanceAsync(derivationStrategy);
|
||||
Console.WriteLine($"New balance: {balance.Unconfirmed}");
|
||||
}
|
||||
|
||||
private static PSBT Sign(Party party, DerivationStrategyBase derivationStrategy, PSBT psbt)
|
||||
{
|
||||
psbt = psbt.Clone();
|
||||
|
||||
// NBXplorer does not have knowledge of the account key path, KeyPath are private information of each peer
|
||||
// NBXplorer only derive 0/* and 1/* on top of provided account xpubs,
|
||||
// This mean that the input keypaths in the PSBT are in the form 0/* (as if the account key was the root)
|
||||
// RebaseKeyPaths modifies the PSBT by adding the AccountKeyPath in prefix of all the keypaths of the PSBT
|
||||
|
||||
// Note that this is not necessary to do this if the account key is the same as root key.
|
||||
// Note that also that you don't have to do this, if you do not pass the account key path in the later SignAll call.
|
||||
// however, this is best practice to rebase the PSBT before signing.
|
||||
// If you sign with an offline device (hw wallet), the wallet would need the rebased PSBT.
|
||||
psbt.RebaseKeyPaths(party.AccountExtPubKey, party.AccountKeyPath);
|
||||
|
||||
Console.WriteLine("A PSBT is a data structure with all information for a wallet to sign.");
|
||||
var spend = psbt.GetBalance(derivationStrategy, party.AccountExtPubKey, party.AccountKeyPath);
|
||||
Console.WriteLine($"{party.PartyName}, Do you agree to sign this transaction spending {spend}?");
|
||||
// Ok I sign
|
||||
psbt.SignAll(derivationStrategy, // What addresses to derive?
|
||||
party.RootExtKey.Derive(party.AccountKeyPath), // With which account private keys?
|
||||
party.AccountKeyPath); // What is the keypath of the account private key. If you did not rebased the keypath like before, you can remove this parameter
|
||||
return psbt;
|
||||
}
|
||||
|
||||
static async Task<NewTransactionEvent> WaitTransaction(LongPollingNotificationSession evts, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var evt = await evts.NextEventAsync();
|
||||
if (evt is NBXplorer.Models.NewTransactionEvent tx)
|
||||
{
|
||||
if (tx.DerivationStrategy == derivationStrategy)
|
||||
return tx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateNBXClient(Network network)
|
||||
{
|
||||
NBXplorerNetworkProvider provider = new NBXplorerNetworkProvider(network.ChainName);
|
||||
ExplorerClient client = new NBXplorer.ExplorerClient(provider.GetFromCryptoCode(network.NetworkSet.CryptoCode));
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Examples/nbxplorer-examples.sln
Normal file
34
Examples/nbxplorer-examples.sln
Normal file
@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26124.0
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiSig", "MultiSig\MultiSig.csproj", "{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DBB077D5-4F0A-4D09-8E73-5A9C56877DDB}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
405
NBXplorer.Client/AssetMoney.cs
Normal file
405
NBXplorer.Client/AssetMoney.cs
Normal file
@ -0,0 +1,405 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class AssetMoney : IComparable, IComparable<AssetMoney>, IEquatable<AssetMoney>, IMoney
|
||||
{
|
||||
long _Quantity;
|
||||
public long Quantity
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Quantity;
|
||||
}
|
||||
// used as a central point where long.MinValue checking can be enforced
|
||||
private set
|
||||
{
|
||||
CheckLongMinValue(value);
|
||||
_Quantity = value;
|
||||
}
|
||||
}
|
||||
private static void CheckLongMinValue(long value)
|
||||
{
|
||||
if (value == long.MinValue)
|
||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
||||
}
|
||||
|
||||
private readonly uint256 _Id;
|
||||
|
||||
/// <summary>
|
||||
/// AssetId of the current amount
|
||||
/// </summary>
|
||||
public uint256 AssetId
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get absolute value of the instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public AssetMoney Abs()
|
||||
{
|
||||
var a = this;
|
||||
if (a.Quantity < 0)
|
||||
a = -a;
|
||||
return a;
|
||||
}
|
||||
#region ctor
|
||||
|
||||
public AssetMoney(uint256 assetId)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
}
|
||||
public AssetMoney(uint256 assetId, int quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, uint quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
public AssetMoney(uint256 assetId, long quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, ulong quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
|
||||
// overflow check.
|
||||
// ulong.MaxValue is greater than long.MaxValue
|
||||
checked
|
||||
{
|
||||
Quantity = (long)quantity;
|
||||
}
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, decimal amount, int divisibility)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
// sanity check. Only valid units are allowed
|
||||
checked
|
||||
{
|
||||
int dec = Pow10(divisibility);
|
||||
var satoshi = amount * dec;
|
||||
Quantity = (long)satoshi;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static int Pow10(int divisibility)
|
||||
{
|
||||
if (divisibility < 0)
|
||||
throw new ArgumentOutOfRangeException("divisibility", "divisibility should be higher than 0");
|
||||
int dec = 1;
|
||||
for (int i = 0; i < divisibility; i++)
|
||||
{
|
||||
dec = dec * 10;
|
||||
}
|
||||
return dec;
|
||||
}
|
||||
|
||||
|
||||
/// <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<AssetMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
|
||||
long remain;
|
||||
long result = DivRem(_Quantity, parts, out remain);
|
||||
|
||||
for (int i = 0; i < parts; i++)
|
||||
{
|
||||
yield return new AssetMoney(_Id, result + (remain > 0 ? 1 : 0));
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
private static long DivRem(long a, long b, out long result)
|
||||
{
|
||||
result = a % b;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
public decimal ToDecimal(int divisibility)
|
||||
{
|
||||
var dec = Pow10(divisibility);
|
||||
// overflow safe because (long / int) always fit in decimal
|
||||
// decimal operations are checked by default
|
||||
return (decimal)Quantity / (int)dec;
|
||||
}
|
||||
|
||||
#region IEquatable<AssetMoney> Members
|
||||
|
||||
public bool Equals(AssetMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
CheckAssetId(other, "other");
|
||||
return _Quantity.Equals(other.Quantity);
|
||||
}
|
||||
|
||||
internal void CheckAssetId(AssetMoney other, string param)
|
||||
{
|
||||
if (other.AssetId != AssetId)
|
||||
throw new ArgumentException("AssetMoney instance of different assets can't be computed together", param);
|
||||
}
|
||||
|
||||
public int CompareTo(AssetMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return 1;
|
||||
CheckAssetId(other, "other");
|
||||
return _Quantity.CompareTo(other.Quantity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
AssetMoney m = obj as AssetMoney;
|
||||
if (m != null)
|
||||
return _Quantity.CompareTo(m.Quantity);
|
||||
#if !NETSTANDARD1X
|
||||
return _Quantity.CompareTo(obj);
|
||||
#else
|
||||
return _Quantity.CompareTo((long)obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static AssetMoney operator -(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return new AssetMoney(left.AssetId, checked(left.Quantity - right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator -(AssetMoney left)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
return new AssetMoney(left.AssetId, checked(-left.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator +(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return new AssetMoney(left.AssetId, checked(left.Quantity + right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(int left, AssetMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(AssetMoney right, int left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(right.Quantity * left));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(long left, AssetMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(AssetMoney right, long left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static bool operator <(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity < right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator >(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity > right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator <=(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity <= right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator >=(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity >= right.Quantity;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
AssetMoney item = obj as AssetMoney;
|
||||
if (item == null)
|
||||
return false;
|
||||
if (item.AssetId != AssetId)
|
||||
return false;
|
||||
return _Quantity.Equals(item.Quantity);
|
||||
}
|
||||
|
||||
public static bool operator ==(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
if (Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
|
||||
if (a.AssetId != b.AssetId)
|
||||
return false;
|
||||
return a.Quantity == b.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator !=(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Tuple.Create(_Quantity, AssetId).GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Format("{0}-{1}", Quantity, AssetId);
|
||||
}
|
||||
|
||||
public static AssetMoney Min(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException(nameof(a));
|
||||
if (b == null)
|
||||
throw new ArgumentNullException(nameof(b));
|
||||
a.CheckAssetId(b, "b");
|
||||
if (a <= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
#region IMoney Members
|
||||
|
||||
|
||||
IMoney IMoney.Add(IMoney money)
|
||||
{
|
||||
var assetMoney = (AssetMoney)money;
|
||||
return this + assetMoney;
|
||||
}
|
||||
|
||||
IMoney IMoney.Sub(IMoney money)
|
||||
{
|
||||
var assetMoney = (AssetMoney)money;
|
||||
return this - assetMoney;
|
||||
}
|
||||
|
||||
IMoney IMoney.Negate()
|
||||
{
|
||||
return this * -1;
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj)
|
||||
{
|
||||
return this.CompareTo(obj);
|
||||
}
|
||||
|
||||
int IComparable<IMoney>.CompareTo(IMoney other)
|
||||
{
|
||||
return this.CompareTo(other);
|
||||
}
|
||||
|
||||
bool IEquatable<IMoney>.Equals(IMoney other)
|
||||
{
|
||||
return this.Equals(other);
|
||||
}
|
||||
|
||||
bool IMoney.IsCompatible(IMoney money)
|
||||
{
|
||||
if (money == null)
|
||||
throw new ArgumentNullException(nameof(money));
|
||||
AssetMoney assetMoney = money as AssetMoney;
|
||||
if (assetMoney == null)
|
||||
return false;
|
||||
return assetMoney.AssetId == AssetId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IMoney Members
|
||||
|
||||
|
||||
IEnumerable<IMoney> IMoney.Split(int parts)
|
||||
{
|
||||
return Split(parts);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
BIN
NBXplorer.Client/Bitcoin.png
Normal file
BIN
NBXplorer.Client/Bitcoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
|
||||
18
NBXplorer.Client/DBUtils.cs
Normal file
18
NBXplorer.Client/DBUtils.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Client
|
||||
{
|
||||
public static class DBUtils
|
||||
{
|
||||
public static string nbxv1_get_wallet_id(string cryptoCode, string addressOrDerivation)
|
||||
{
|
||||
return Encoders.Base64.EncodeData(Hashes.SHA256(new UTF8Encoding(false).GetBytes($"{cryptoCode}|{addressOrDerivation}")), 0, 21);
|
||||
}
|
||||
public static string nbxv1_get_descriptor_id(string cryptoCode, string strategy, string feature)
|
||||
{
|
||||
return Encoders.Base64.EncodeData(Hashes.SHA256(new UTF8Encoding(false).GetBytes($"{cryptoCode}|{strategy}|{feature}")), 0, 21);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,49 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
#if !NO_RECORD
|
||||
using static NBitcoin.WalletPolicies.MiniscriptNode;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class Derivation
|
||||
{
|
||||
public Derivation(Script scriptPubKey, Script? redeem = null)
|
||||
{
|
||||
ScriptPubKey = scriptPubKey;
|
||||
Redeem = redeem;
|
||||
}
|
||||
public Script ScriptPubKey
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
}
|
||||
public Script Redeem
|
||||
public Script? Redeem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyPathDerivation : Derivation
|
||||
{
|
||||
public KeyPathDerivation(KeyPath keyPath, Script scriptPubKey, Script? redeem = null)
|
||||
: base(scriptPubKey, redeem)
|
||||
{
|
||||
KeyPath = keyPath;
|
||||
}
|
||||
|
||||
public KeyPath KeyPath { get; }
|
||||
}
|
||||
#if !NO_RECORD
|
||||
public class PolicyDerivation : Derivation
|
||||
{
|
||||
public PolicyDerivation(NBitcoin.WalletPolicies.DerivationResult details, Script scriptPubKey, Script? redeem = null)
|
||||
: base(scriptPubKey, redeem)
|
||||
{
|
||||
Details = details;
|
||||
}
|
||||
|
||||
public NBitcoin.WalletPolicies.DerivationResult Details { get; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1,32 +1,19 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class DerivationStrategyOptions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// If true, use P2SH (default: false)
|
||||
/// </summary>
|
||||
public bool P2SH
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If false, use segwit (default: false)
|
||||
/// </summary>
|
||||
public bool Legacy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ScriptPubKeyType ScriptPubKeyType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, in case of multisig, do not reorder the public keys of an address lexicographically (default: false)
|
||||
@ -35,6 +22,8 @@ namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ReadOnlyDictionary<string, string> AdditionalOptions { get; set; }
|
||||
}
|
||||
public class DerivationStrategyFactory
|
||||
{
|
||||
@ -49,13 +38,24 @@ namespace NBXplorer.DerivationStrategy
|
||||
}
|
||||
public DerivationStrategyFactory(Network network)
|
||||
{
|
||||
if(network == null)
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
_Network = network;
|
||||
if (_Network.Consensus.SupportSegwit)
|
||||
{
|
||||
AuthorizedOptions.Add("p2sh");
|
||||
}
|
||||
if (_Network.Consensus.SupportTaproot)
|
||||
{
|
||||
AuthorizedOptions.Add("taproot");
|
||||
}
|
||||
AuthorizedOptions.Add("keeporder");
|
||||
AuthorizedOptions.Add("legacy");
|
||||
}
|
||||
|
||||
public HashSet<string> AuthorizedOptions { get; } = new HashSet<string>();
|
||||
|
||||
readonly Regex MultiSigRegex = new Regex("^([0-9]{1,2})-of(-[A-Za-z0-9]+)+$");
|
||||
static DirectDerivationStrategy DummyPubKey = new DirectDerivationStrategy(new ExtKey().Neuter().GetWif(Network.RegTest)) { Segwit = false };
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
var strategy = ParseCore(str);
|
||||
@ -65,25 +65,78 @@ namespace NBXplorer.DerivationStrategy
|
||||
private DerivationStrategyBase ParseCore(string str)
|
||||
{
|
||||
bool legacy = false;
|
||||
ReadBool(ref str, "legacy", ref legacy);
|
||||
|
||||
bool p2sh = false;
|
||||
ReadBool(ref str, "p2sh", ref p2sh);
|
||||
|
||||
bool keepOrder = false;
|
||||
ReadBool(ref str, "keeporder", ref keepOrder);
|
||||
bool taproot = false;
|
||||
ScriptPubKeyType type = ScriptPubKeyType.Segwit;
|
||||
|
||||
if(!legacy && !_Network.Consensus.SupportSegwit)
|
||||
throw new FormatException("Segwit is not supported");
|
||||
IDictionary<string, string> optionsDictionary = new Dictionary<string, string>(5);
|
||||
foreach (Match optionMatch in _OptionRegex.Matches(str))
|
||||
{
|
||||
var rawKey = optionMatch.Groups[1].Value.ToLowerInvariant();
|
||||
var splitKey = rawKey.Split(new[]{'='}, StringSplitOptions.RemoveEmptyEntries);
|
||||
var key = splitKey[0];
|
||||
var value = splitKey.Length > 1 ? splitKey[1]: null;
|
||||
if (!AuthorizedOptions.Contains(key))
|
||||
throw new FormatException($"The option '{key}' is not supported by this network");
|
||||
if (!Extensions.TryAdd(optionsDictionary, key, value))
|
||||
throw new FormatException($"The option '{key}' is duplicated");
|
||||
}
|
||||
|
||||
var hasOptions = optionsDictionary.Count != 0;
|
||||
str = _OptionRegex.Replace(str, string.Empty);
|
||||
if (optionsDictionary.Remove("legacy"))
|
||||
{
|
||||
legacy = true;
|
||||
type = ScriptPubKeyType.Legacy;
|
||||
}
|
||||
if (optionsDictionary.Remove("p2sh"))
|
||||
{
|
||||
p2sh = true;
|
||||
type = ScriptPubKeyType.SegwitP2SH;
|
||||
}
|
||||
if (optionsDictionary.Remove("keeporder"))
|
||||
{
|
||||
keepOrder = true;
|
||||
}
|
||||
if (optionsDictionary.Remove("taproot"))
|
||||
{
|
||||
taproot = true;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
type = ScriptPubKeyType.TaprootBIP86;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
if (!legacy && !_Network.Consensus.SupportSegwit)
|
||||
throw new FormatException("Segwit is not supported you need to specify option '-[legacy]'");
|
||||
|
||||
if (legacy && p2sh)
|
||||
throw new FormatException("The option 'legacy' is incompatible with 'p2sh'");
|
||||
|
||||
if (taproot)
|
||||
{
|
||||
if (!_Network.Consensus.SupportTaproot)
|
||||
{
|
||||
throw new FormatException("Taproot is not supported, you need to remove option '-[taproot]'");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (p2sh)
|
||||
throw new FormatException("The option 'taproot' is incompatible with 'p2sh'");
|
||||
if (legacy)
|
||||
throw new FormatException("The option 'taproot' is incompatible with 'legacy'");
|
||||
if (keepOrder)
|
||||
throw new FormatException("The option 'taproot' is incompatible with 'keeporder'");
|
||||
}
|
||||
}
|
||||
|
||||
var options = new DerivationStrategyOptions()
|
||||
{
|
||||
KeepOrder = keepOrder,
|
||||
Legacy = legacy,
|
||||
P2SH = p2sh
|
||||
ScriptPubKeyType = type,
|
||||
AdditionalOptions = new ReadOnlyDictionary<string, string>(optionsDictionary)
|
||||
};
|
||||
var match = MultiSigRegex.Match(str);
|
||||
if(match.Success)
|
||||
if (match.Success)
|
||||
{
|
||||
var sigCount = int.Parse(match.Groups[1].Value);
|
||||
var pubKeys = match.Groups
|
||||
@ -94,6 +147,14 @@ namespace NBXplorer.DerivationStrategy
|
||||
.ToArray();
|
||||
return CreateMultiSigDerivationStrategy(pubKeys, sigCount, options);
|
||||
}
|
||||
#if !NO_RECORD
|
||||
else if (PolicyDerivationStrategy._MaybeMiniscript.IsMatch(str))
|
||||
{
|
||||
if (hasOptions)
|
||||
throw new FormatException("The derivation scheme should not contain any option (such as -[legacy])");
|
||||
return PolicyDerivationStrategy.Parse(str, _Network);
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
var key = _Network.Parse<BitcoinExtPubKey>(str);
|
||||
@ -107,7 +168,7 @@ namespace NBXplorer.DerivationStrategy
|
||||
/// <param name="publicKey">The public key of the wallet</param>
|
||||
/// <param name="options">Derivation options</param>
|
||||
/// <returns></returns>
|
||||
public DerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
{
|
||||
return CreateDirectDerivationStrategy(publicKey.GetWif(Network), options);
|
||||
}
|
||||
@ -118,19 +179,41 @@ namespace NBXplorer.DerivationStrategy
|
||||
/// <param name="publicKey">The public key of the wallet</param>
|
||||
/// <param name="options">Derivation options</param>
|
||||
/// <returns></returns>
|
||||
public DerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
{
|
||||
options = options ?? new DerivationStrategyOptions();
|
||||
DerivationStrategyBase strategy = new DirectDerivationStrategy(publicKey) { Segwit = !options.Legacy };
|
||||
if(!options.Legacy && !_Network.Consensus.SupportSegwit)
|
||||
throw new InvalidOperationException("This crypto currency does not support segwit");
|
||||
|
||||
if(options.P2SH && !options.Legacy)
|
||||
StandardDerivationStrategyBase strategy = null;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (options.ScriptPubKeyType != ScriptPubKeyType.TaprootBIP86)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
strategy = new P2SHDerivationStrategy(strategy, true);
|
||||
strategy = new DirectDerivationStrategy(publicKey, options.ScriptPubKeyType != ScriptPubKeyType.Legacy, options.AdditionalOptions);
|
||||
if (options.ScriptPubKeyType == ScriptPubKeyType.Segwit && !_Network.Consensus.SupportSegwit)
|
||||
throw new InvalidOperationException("This crypto currency does not support segwit");
|
||||
|
||||
if (options.ScriptPubKeyType == ScriptPubKeyType.SegwitP2SH)
|
||||
{
|
||||
strategy = new P2SHDerivationStrategy(strategy, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_Network.Consensus.SupportTaproot)
|
||||
throw new InvalidOperationException("This crypto currency does not support taproot");
|
||||
strategy = new TaprootDerivationStrategy(publicKey, options.AdditionalOptions);
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a taproot signature derivation strategy from public key
|
||||
/// </summary>
|
||||
/// <param name="publicKey">The public key of the wallet</param>
|
||||
/// <param name="options">Derivation options</param>
|
||||
/// <returns></returns>
|
||||
public TaprootDerivationStrategy CreateTaprootDerivationStrategy(BitcoinExtPubKey publicKey, ReadOnlyDictionary<string, string> options = null)
|
||||
{
|
||||
return new TaprootDerivationStrategy(publicKey, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a multisig derivation strategy from public keys
|
||||
@ -154,33 +237,19 @@ namespace NBXplorer.DerivationStrategy
|
||||
public DerivationStrategyBase CreateMultiSigDerivationStrategy(BitcoinExtPubKey[] pubKeys, int sigCount, DerivationStrategyOptions options = null)
|
||||
{
|
||||
options = options ?? new DerivationStrategyOptions();
|
||||
DerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.Legacy)
|
||||
{
|
||||
LexicographicOrder = !options.KeepOrder
|
||||
};
|
||||
if(options.Legacy)
|
||||
StandardDerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
|
||||
if (options.ScriptPubKeyType == ScriptPubKeyType.Legacy)
|
||||
return new P2SHDerivationStrategy(derivationStrategy, false);
|
||||
|
||||
if(!_Network.Consensus.SupportSegwit)
|
||||
if (!_Network.Consensus.SupportSegwit)
|
||||
throw new InvalidOperationException("This crypto currency does not support segwit");
|
||||
derivationStrategy = new P2WSHDerivationStrategy(derivationStrategy);
|
||||
if(options.P2SH)
|
||||
if (options.ScriptPubKeyType == ScriptPubKeyType.SegwitP2SH)
|
||||
{
|
||||
derivationStrategy = new P2SHDerivationStrategy(derivationStrategy, true);
|
||||
}
|
||||
return derivationStrategy;
|
||||
}
|
||||
|
||||
private void ReadBool(ref string str, string attribute, ref bool value)
|
||||
{
|
||||
value = str.Contains($"[{attribute}]");
|
||||
if(value)
|
||||
{
|
||||
str = str.Replace($"[{attribute}]", string.Empty);
|
||||
str = str.Replace("--", "-");
|
||||
if(str.EndsWith("-"))
|
||||
str = str.Substring(0, str.Length - 1);
|
||||
}
|
||||
}
|
||||
readonly static Regex _OptionRegex = new Regex(@"-\[([^ \]\-]+)\]");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class DirectDerivationStrategy : DerivationStrategyBase
|
||||
public class DirectDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
BitcoinExtPubKey _Root;
|
||||
|
||||
@ -21,10 +21,9 @@ namespace NBXplorer.DerivationStrategy
|
||||
public bool Segwit
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
protected override string StringValue
|
||||
protected internal override string StringValueCore
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -38,21 +37,23 @@ namespace NBXplorer.DerivationStrategy
|
||||
}
|
||||
}
|
||||
|
||||
public DirectDerivationStrategy(BitcoinExtPubKey root)
|
||||
public DirectDerivationStrategy(BitcoinExtPubKey root, bool segwit, ReadOnlyDictionary<string, string> additionalOptions = null) : base(additionalOptions)
|
||||
{
|
||||
if(root == null)
|
||||
throw new ArgumentNullException(nameof(root));
|
||||
_Root = root;
|
||||
}
|
||||
public override Derivation Derive(KeyPath keyPath)
|
||||
{
|
||||
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey;
|
||||
return new Derivation() { ScriptPubKey = Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey };
|
||||
Segwit = segwit;
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetLineFor(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new DirectDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network)) { Segwit = Segwit };
|
||||
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey;
|
||||
return new KeyPathDerivation(keyPath, Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey);
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
yield return _Root.ExtPubKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
@ -10,72 +14,132 @@ namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
Change = 1,
|
||||
Deposit = 0,
|
||||
Direct = 2
|
||||
}
|
||||
Direct = 2,
|
||||
Custom = 3,
|
||||
}
|
||||
|
||||
public abstract class StandardDerivationStrategyBase : DerivationStrategyBase, IHDScriptPubKey
|
||||
{
|
||||
internal StandardDerivationStrategyBase(ReadOnlyDictionary<string, string> additionalOptions) : base(additionalOptions)
|
||||
{
|
||||
}
|
||||
public abstract Derivation GetDerivation(KeyPath keyPath);
|
||||
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature)
|
||||
=> new KeyPathTemplateDerivationLine(this, keyPathTemplates, feature);
|
||||
Script IHDScriptPubKey.ScriptPubKey => GetDerivation(KeyPath.Empty).ScriptPubKey;
|
||||
IHDScriptPubKey? IHDScriptPubKey.Derive(KeyPath keyPath) => keyPath.IsHardenedPath ? null : new HDScriptPubKey(this, keyPath);
|
||||
class HDScriptPubKey(StandardDerivationStrategyBase Parent, KeyPath KeyPath) : IHDScriptPubKey
|
||||
{
|
||||
public Script ScriptPubKey => Parent.GetDerivation(KeyPath).ScriptPubKey;
|
||||
public IHDScriptPubKey? Derive(KeyPath keyPath) => KeyPath.IsHardenedPath ? null : new HDScriptPubKey(Parent, KeyPath.Derive(keyPath));
|
||||
}
|
||||
}
|
||||
public abstract class DerivationStrategyBase
|
||||
{
|
||||
internal DerivationStrategyBase()
|
||||
{
|
||||
readonly ReadOnlyDictionary<string, string> Empty = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0));
|
||||
public ReadOnlyDictionary<string, string> AdditionalOptions { get; }
|
||||
|
||||
internal DerivationStrategyBase(ReadOnlyDictionary<string,string>? additionalOptions)
|
||||
{
|
||||
AdditionalOptions = additionalOptions ?? Empty;
|
||||
}
|
||||
|
||||
public static KeyPath GetKeyPath(DerivationFeature derivationFeature)
|
||||
{
|
||||
return derivationFeature == DerivationFeature.Direct ? new KeyPath() : new KeyPath((uint)derivationFeature);
|
||||
}
|
||||
public static DerivationFeature GetFeature(KeyPath path)
|
||||
{
|
||||
return path.Indexes.Length == 1 ? DerivationFeature.Direct : (DerivationFeature)path.Indexes[0];
|
||||
}
|
||||
public DerivationStrategyBase GetLineFor(DerivationFeature derivationFeature)
|
||||
{
|
||||
return derivationFeature == DerivationFeature.Direct ? this :
|
||||
GetLineFor(GetKeyPath(derivationFeature));
|
||||
}
|
||||
public DerivationLine GetLineFor(DerivationFeature feature) => GetLineFor(KeyPathTemplates.Default, feature);
|
||||
public abstract DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature);
|
||||
|
||||
public abstract DerivationStrategyBase GetLineFor(KeyPath keyPath);
|
||||
|
||||
public Derivation Derive(uint i)
|
||||
{
|
||||
return Derive(new KeyPath(i));
|
||||
}
|
||||
public abstract Derivation Derive(KeyPath keyPath);
|
||||
|
||||
protected abstract string StringValue
|
||||
protected internal abstract string StringValueCore
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
string? _StringValue;
|
||||
string StringValue
|
||||
{
|
||||
DerivationStrategyBase item = obj as DerivationStrategyBase;
|
||||
if(item == null)
|
||||
return false;
|
||||
return StringValue.Equals(item.StringValue);
|
||||
}
|
||||
public static bool operator ==(DerivationStrategyBase a, DerivationStrategyBase b)
|
||||
{
|
||||
if(System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if(((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.StringValue == b.StringValue;
|
||||
get
|
||||
{
|
||||
if (_StringValue == null)
|
||||
{
|
||||
if (AdditionalOptions.Count == 0)
|
||||
_StringValue = StringValueCore;
|
||||
else
|
||||
_StringValue = $"{StringValueCore}{GetSuffixOptionsString()}";
|
||||
}
|
||||
return _StringValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool operator !=(DerivationStrategyBase a, DerivationStrategyBase b)
|
||||
private string GetSuffixOptionsString()
|
||||
{
|
||||
return !(a == b);
|
||||
return string.Join("", new SortedDictionary<string, string>(AdditionalOptions).Select(pair => $"-[{pair.Key}{(string.IsNullOrEmpty(pair.Value)?string.Empty: $"={pair.Value}")}]"));
|
||||
}
|
||||
#nullable enable
|
||||
public override bool Equals(object? obj) => obj is DerivationStrategyBase o && StringValue.Equals(o.StringValue);
|
||||
public static bool operator ==(DerivationStrategyBase? a, DerivationStrategyBase? b) => a is null ? b is null : a.Equals(b);
|
||||
public static bool operator !=(DerivationStrategyBase? a, DerivationStrategyBase? b) => !(a == b);
|
||||
public override int GetHashCode() => StringValue.GetHashCode();
|
||||
#nullable restore
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringValue.GetHashCode();
|
||||
}
|
||||
public abstract IEnumerable<ExtPubKey> GetExtPubKeys();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return StringValue;
|
||||
}
|
||||
}
|
||||
|
||||
#if !NO_RECORD
|
||||
public class MiniscriptDerivationLine : DerivationLine
|
||||
{
|
||||
public MiniscriptDerivationLine(PolicyDerivationStrategy derivationStrategy, DerivationFeature derivationFeature) : base(derivationFeature)
|
||||
{
|
||||
DerivationStrategy = derivationStrategy;
|
||||
Intent = ToAddressIntent(derivationFeature);
|
||||
}
|
||||
|
||||
public static AddressIntent ToAddressIntent(DerivationFeature derivationFeature)
|
||||
{
|
||||
return derivationFeature switch
|
||||
{
|
||||
DerivationFeature.Change => AddressIntent.Change,
|
||||
DerivationFeature.Deposit => AddressIntent.Deposit,
|
||||
_ => throw new NotSupportedException("MiniscriptDerivationStrategy only support deposit and change features")
|
||||
};
|
||||
}
|
||||
|
||||
public PolicyDerivationStrategy DerivationStrategy { get; }
|
||||
public AddressIntent Intent { get; }
|
||||
|
||||
public override Derivation Derive(uint index) => DerivationStrategy.GetDerivation(Intent, index);
|
||||
}
|
||||
#endif
|
||||
public abstract class DerivationLine
|
||||
{
|
||||
protected DerivationLine(DerivationFeature feature)
|
||||
{
|
||||
Feature = feature;
|
||||
}
|
||||
public DerivationFeature Feature { get; }
|
||||
public abstract Derivation Derive(uint index);
|
||||
}
|
||||
public class KeyPathTemplateDerivationLine : DerivationLine
|
||||
{
|
||||
public KeyPathTemplateDerivationLine(StandardDerivationStrategyBase derivationStrategyBase, KeyPathTemplates keyPathTemplates, DerivationFeature derivationFeature) : base(derivationFeature)
|
||||
{
|
||||
if (derivationStrategyBase == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategyBase));
|
||||
if (keyPathTemplates == null)
|
||||
throw new ArgumentNullException(nameof(keyPathTemplates));
|
||||
DerivationStrategyBase = derivationStrategyBase;
|
||||
KeyPathTemplate = keyPathTemplates.GetKeyPathTemplate(derivationFeature);
|
||||
}
|
||||
|
||||
public StandardDerivationStrategyBase DerivationStrategyBase { get; }
|
||||
public KeyPathTemplate KeyPathTemplate { get; }
|
||||
|
||||
public override Derivation Derive(uint index)
|
||||
{
|
||||
var kp = KeyPathTemplate.GetKeyPath(index);
|
||||
return DerivationStrategyBase.GetDerivation(kp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
using NBitcoin;
|
||||
using System.Linq;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using NBitcoin.Crypto;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class MultisigDerivationStrategy : DerivationStrategyBase
|
||||
public class MultisigDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
public bool LexicographicOrder
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
}
|
||||
|
||||
public int RequiredSignatures
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
}
|
||||
|
||||
static readonly Comparer<PubKey> LexicographicComparer = Comparer<PubKey>.Create((a, b) => Comparer<string>.Default.Compare(a?.ToHex(), b?.ToHex()));
|
||||
|
||||
public BitcoinExtPubKey[] Keys
|
||||
public ReadOnlyCollection<BitcoinExtPubKey> Keys
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
}
|
||||
|
||||
protected override string StringValue
|
||||
protected internal override string StringValueCore
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -48,41 +48,38 @@ namespace NBXplorer.DerivationStrategy
|
||||
}
|
||||
}
|
||||
|
||||
internal MultisigDerivationStrategy(int reqSignature, BitcoinExtPubKey[] keys, bool isLegacy)
|
||||
internal MultisigDerivationStrategy(int reqSignature, BitcoinExtPubKey[] keys, bool isLegacy, bool lexicographicOrder,
|
||||
ReadOnlyDictionary<string, string> additionalOptions) : base(additionalOptions)
|
||||
{
|
||||
Keys = keys;
|
||||
Keys = new ReadOnlyCollection<BitcoinExtPubKey>(keys);
|
||||
RequiredSignatures = reqSignature;
|
||||
LexicographicOrder = true;
|
||||
LexicographicOrder = lexicographicOrder;
|
||||
IsLegacy = isLegacy;
|
||||
}
|
||||
|
||||
public bool IsLegacy
|
||||
{
|
||||
get; private set;
|
||||
get;
|
||||
}
|
||||
|
||||
private void WriteBytes(MemoryStream ms, byte[] v)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
ms.Write(v, 0, v.Length);
|
||||
}
|
||||
|
||||
public override Derivation Derive(KeyPath keyPath)
|
||||
{
|
||||
var pubKeys = this.Keys.Select(s => s.ExtPubKey.Derive(keyPath).PubKey).ToArray();
|
||||
var pubKeys = new PubKey[this.Keys.Count];
|
||||
Parallel.For(0, pubKeys.Length, i =>
|
||||
{
|
||||
pubKeys[i] = this.Keys[i].ExtPubKey.Derive(keyPath).PubKey;
|
||||
});
|
||||
if(LexicographicOrder)
|
||||
{
|
||||
Array.Sort(pubKeys, LexicographicComparer);
|
||||
}
|
||||
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(RequiredSignatures, pubKeys);
|
||||
return new Derivation() { ScriptPubKey = redeem };
|
||||
return new KeyPathDerivation(keyPath, redeem);
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetLineFor(KeyPath keyPath)
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
return new MultisigDerivationStrategy(RequiredSignatures, Keys.Select(k => k.ExtPubKey.Derive(keyPath).GetWif(k.Network)).ToArray(), IsLegacy)
|
||||
{
|
||||
LexicographicOrder = LexicographicOrder
|
||||
};
|
||||
return Keys.Select(k => k.ExtPubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class P2SHDerivationStrategy : DerivationStrategyBase
|
||||
public class P2SHDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
bool addSuffix;
|
||||
internal P2SHDerivationStrategy(DerivationStrategyBase inner, bool addSuffix)
|
||||
internal P2SHDerivationStrategy(StandardDerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
|
||||
{
|
||||
if(inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
@ -19,34 +15,33 @@ namespace NBXplorer.DerivationStrategy
|
||||
this.addSuffix = addSuffix;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Inner
|
||||
public StandardDerivationStrategyBase Inner
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override string StringValue
|
||||
protected internal override string StringValueCore
|
||||
{
|
||||
get
|
||||
{
|
||||
if(addSuffix)
|
||||
return Inner.ToString() + "-[p2sh]";
|
||||
return Inner.StringValueCore + "-[p2sh]";
|
||||
return Inner.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public override Derivation Derive(KeyPath keyPath)
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
var derivation = Inner.Derive(keyPath);
|
||||
return new Derivation()
|
||||
{
|
||||
ScriptPubKey = derivation.ScriptPubKey.Hash.ScriptPubKey,
|
||||
Redeem = derivation.Redeem ?? derivation.ScriptPubKey
|
||||
};
|
||||
return Inner.GetExtPubKeys();
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetLineFor(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new P2SHDerivationStrategy(Inner.GetLineFor(keyPath), addSuffix);
|
||||
var derivation = Inner.GetDerivation(keyPath);
|
||||
return new KeyPathDerivation(
|
||||
keyPath,
|
||||
derivation.ScriptPubKey.Hash.ScriptPubKey,
|
||||
derivation.Redeem ?? derivation.ScriptPubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,42 +1,34 @@
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class P2WSHDerivationStrategy : DerivationStrategyBase
|
||||
public class P2WSHDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
internal P2WSHDerivationStrategy(DerivationStrategyBase inner)
|
||||
internal P2WSHDerivationStrategy(StandardDerivationStrategyBase inner):base(inner.AdditionalOptions)
|
||||
{
|
||||
if(inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
Inner = inner;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Inner
|
||||
public StandardDerivationStrategyBase Inner
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override string StringValue => Inner.ToString();
|
||||
protected internal override string StringValueCore => Inner.ToString();
|
||||
|
||||
public override Derivation Derive(KeyPath keyPath)
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
var derivation = Inner.Derive(keyPath);
|
||||
return new Derivation()
|
||||
{
|
||||
ScriptPubKey = derivation.ScriptPubKey.WitHash.ScriptPubKey,
|
||||
Redeem = derivation.ScriptPubKey
|
||||
};
|
||||
return Inner.GetExtPubKeys();
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetLineFor(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new P2WSHDerivationStrategy(Inner.GetLineFor(keyPath));
|
||||
var redeem = Inner.GetDerivation(keyPath).ScriptPubKey;
|
||||
return new KeyPathDerivation(keyPath, redeem.WitHash.ScriptPubKey, redeem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
NBXplorer.Client/DerivationStrategy/PolicyDerivationStrategy.cs
Normal file
212
NBXplorer.Client/DerivationStrategy/PolicyDerivationStrategy.cs
Normal file
@ -0,0 +1,212 @@
|
||||
#nullable enable
|
||||
#if !NO_RECORD
|
||||
using NBitcoin;
|
||||
using NBitcoin.WalletPolicies;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class PolicyDerivationStrategy : DerivationStrategyBase
|
||||
{
|
||||
internal static readonly Regex _MaybeMiniscript = new("^(wsh|sh|pkh|tr|wpkh)\\(");
|
||||
public static bool TryParse(
|
||||
string str,
|
||||
Network network,
|
||||
[MaybeNullWhen(false)] out PolicyDerivationStrategy strategy)
|
||||
{
|
||||
strategy = null;
|
||||
if (!_MaybeMiniscript.IsMatch(str))
|
||||
return false;
|
||||
if (!WalletPolicy.TryParse(str, network, out var policy) || !IsValidPolicy(policy, out _))
|
||||
return false;
|
||||
strategy = new PolicyDerivationStrategy(policy, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PolicyDerivationStrategy Parse(string str, Network network)
|
||||
{
|
||||
if (!_MaybeMiniscript.IsMatch(str))
|
||||
throw new FormatException("The policy should start by either wsh, sh, pkh, tr, wpkh");
|
||||
var policy = WalletPolicy.Parse(str, network);
|
||||
if (!IsValidPolicy(policy, out var err))
|
||||
throw new FormatException(err);
|
||||
return new PolicyDerivationStrategy(policy, false);
|
||||
}
|
||||
|
||||
public PolicyDerivationStrategy(WalletPolicy policy) : this(policy, true)
|
||||
{
|
||||
}
|
||||
|
||||
PolicyDerivationStrategy(WalletPolicy policy, bool check) : base(null)
|
||||
{
|
||||
if (check && !IsValidPolicy(policy, out var error))
|
||||
throw new ArgumentException(paramName: nameof(policy), message: error);
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that the policy should have at least one multi path node ([12345678]xpub/**) and no xpriv
|
||||
/// </summary>
|
||||
/// <param name="policy"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public static bool IsValidPolicy(WalletPolicy policy, [MaybeNullWhen(true)] out string error)
|
||||
{
|
||||
var v = new ValidPolicyVisitor();
|
||||
policy.FullDescriptor.Visit(v);
|
||||
error = v.Error;
|
||||
return error is null;
|
||||
}
|
||||
|
||||
class ValidPolicyVisitor : MiniscriptVisitor
|
||||
{
|
||||
public string? Error {
|
||||
get
|
||||
{
|
||||
if (hasSecretKey)
|
||||
return "The policy should not contain any xpriv key";
|
||||
if (!hasMultiPathNode)
|
||||
return "The policy should contain at least one multi path node ([12345678]xpub/**)";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private bool hasMultiPathNode;
|
||||
private bool hasSecretKey;
|
||||
|
||||
public override void Visit(MiniscriptNode node)
|
||||
{
|
||||
if (node is MiniscriptNode.MultipathNode)
|
||||
hasMultiPathNode = true;
|
||||
else if (node is MiniscriptNode.HDKeyNode { Key: BitcoinExtKey })
|
||||
hasSecretKey = true;
|
||||
else
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
public WalletPolicy Policy { get; }
|
||||
private readonly DerivationCache cache = new();
|
||||
private string? _str;
|
||||
|
||||
protected internal override string StringValueCore => _str ??= Policy.ToString(true);
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
=> Policy.KeyInformationVector.Select(kv => GetExtPubKey(kv.Key));
|
||||
|
||||
private ExtPubKey GetExtPubKey(IHDKey key)
|
||||
=> key switch
|
||||
{
|
||||
ExtPubKey extPubKey => extPubKey,
|
||||
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
|
||||
ExtKey extKey => extKey.Neuter(),
|
||||
BitcoinExtKey bitcoinExtKey => bitcoinExtKey.ExtKey.Neuter(),
|
||||
_ => throw new NotSupportedException($"Unsupported key type: {key.GetType()}")
|
||||
};
|
||||
public NBXplorer.DerivationStrategy.Derivation GetDerivation(DerivationFeature feature, uint index)
|
||||
=> GetDerivation(MiniscriptDerivationLine.ToAddressIntent(feature), index);
|
||||
public NBXplorer.DerivationStrategy.Derivation GetDerivation(AddressIntent addressIntent, uint index)
|
||||
{
|
||||
var derived = Policy.FullDescriptor.Derive(new(addressIntent, [(int)index]) { DervivationCache = cache });
|
||||
var scripts = derived[0].Miniscript.ToScripts();
|
||||
return new PolicyDerivation(derived[0], scripts.ScriptPubKey, scripts.RedeemScript);
|
||||
}
|
||||
|
||||
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature) => new MiniscriptDerivationLine(this, feature);
|
||||
|
||||
// Extract the multipath node from the hdkey
|
||||
class MultipathNodeVisitor : MiniscriptVisitor
|
||||
{
|
||||
private readonly ExtPubKey _target;
|
||||
public MiniscriptNode.MultipathNode? Result { get; set; }
|
||||
public MultipathNodeVisitor(IHDKey target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
_target = Normalize(target);
|
||||
}
|
||||
|
||||
private static ExtPubKey Normalize(IHDKey target)
|
||||
=> target switch
|
||||
{
|
||||
BitcoinExtKey extKey => extKey.Neuter().ExtPubKey,
|
||||
ExtKey extKey => extKey.Neuter(),
|
||||
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
|
||||
ExtPubKey a => a,
|
||||
_ => throw new NotSupportedException(target.GetType().ToString())
|
||||
};
|
||||
|
||||
public override void Visit(MiniscriptNode node)
|
||||
{
|
||||
if (Result is not null)
|
||||
return;
|
||||
if (node is MiniscriptNode.MultipathNode { Target: MiniscriptNode.HDKeyNode hd } mp)
|
||||
{
|
||||
if (Normalize(hd.Key).Equals(_target))
|
||||
Result = mp;
|
||||
}
|
||||
else
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
class MiniscriptScriptPubKey : IHDScriptPubKey
|
||||
{
|
||||
private readonly PolicyDerivationStrategy _policyDerivationStrategy;
|
||||
private readonly MiniscriptNode.MultipathNode _multipathNode;
|
||||
private readonly KeyPath _keyPath;
|
||||
|
||||
public MiniscriptScriptPubKey(
|
||||
PolicyDerivationStrategy policyDerivationStrategy,
|
||||
MiniscriptNode.MultipathNode multipathNode,
|
||||
KeyPath? keyPath = null,
|
||||
DerivationCache? cache = null)
|
||||
{
|
||||
_policyDerivationStrategy = policyDerivationStrategy;
|
||||
_multipathNode = multipathNode;
|
||||
_keyPath = keyPath ?? KeyPath.Empty;
|
||||
_cache = cache ?? new();
|
||||
}
|
||||
|
||||
private readonly DerivationCache _cache;
|
||||
public IHDScriptPubKey? Derive(KeyPath keyPath) =>
|
||||
_keyPath.Derive(keyPath) is { Length: <= 2 } kp
|
||||
&& (kp.Length == 0 || GetAddressIntent(kp.Indexes[0]) is not null)
|
||||
&& !kp.IsHardenedPath
|
||||
? new MiniscriptScriptPubKey(_policyDerivationStrategy, _multipathNode, kp, _cache) : null;
|
||||
|
||||
public Script ScriptPubKey
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_keyPath is not { Indexes: [var intentIdx, var index], IsHardenedPath: false }
|
||||
|| GetAddressIntent(intentIdx) is not {} intent)
|
||||
throw new InvalidOperationException("Invalid keypath (it should be non hardened with two component)");
|
||||
var derived = _policyDerivationStrategy.Policy.FullDescriptor.Derive(new(intent, new[] { (int)index })
|
||||
{
|
||||
DervivationCache = _cache
|
||||
});
|
||||
return derived[0].Miniscript.ToScripts().ScriptPubKey;
|
||||
}
|
||||
}
|
||||
|
||||
private AddressIntent? GetAddressIntent(uint intentIdx)
|
||||
=> intentIdx == _multipathNode.DepositIndex ? AddressIntent.Deposit :
|
||||
intentIdx == _multipathNode.ChangeIndex ? AddressIntent.Change : null;
|
||||
}
|
||||
|
||||
public IHDScriptPubKey? GetHDScriptPubKey(IHDKey accountKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountKey);
|
||||
var visitor = new MultipathNodeVisitor(accountKey);
|
||||
visitor.Visit(Policy.FullDescriptor.RootNode);
|
||||
if (visitor.Result is null)
|
||||
return null;
|
||||
return new MiniscriptScriptPubKey(this, visitor.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class TaprootDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
BitcoinExtPubKey _Root;
|
||||
|
||||
public ExtPubKey Root
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Root;
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override string StringValueCore
|
||||
{
|
||||
get
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append(_Root.ToString());
|
||||
builder.Append("-[taproot]");
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public TaprootDerivationStrategy(BitcoinExtPubKey root, ReadOnlyDictionary<string, string> additionalOptions = null) : base(additionalOptions)
|
||||
{
|
||||
if (root == null)
|
||||
throw new ArgumentNullException(nameof(root));
|
||||
_Root = root;
|
||||
}
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
#if NO_SPAN
|
||||
throw new NotSupportedException("Deriving taproot address is not supported on this platform.");
|
||||
#else
|
||||
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey.GetTaprootFullPubKey();
|
||||
return new KeyPathDerivation(keyPath, pubKey.ScriptPubKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
yield return _Root.ExtPubKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
using NBitcoin;
|
||||
using System.Linq;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
@ -14,12 +12,15 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.WebSockets;
|
||||
using NBitcoin.RPC;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class ExplorerClient
|
||||
{
|
||||
internal interface IAuth
|
||||
public interface IAuth
|
||||
{
|
||||
bool RefreshCache();
|
||||
void SetAuthorization(HttpRequestMessage message);
|
||||
@ -36,11 +37,13 @@ namespace NBXplorer
|
||||
_CookieFilePath = path;
|
||||
}
|
||||
|
||||
public string CookieFilePath => _CookieFilePath;
|
||||
|
||||
public bool RefreshCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cookieData = File.ReadAllText(_CookieFilePath);
|
||||
var cookieData = File.ReadAllText(CookieFilePath);
|
||||
_CachedAuth = new AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(cookieData)));
|
||||
return true;
|
||||
}
|
||||
@ -54,7 +57,7 @@ namespace NBXplorer
|
||||
|
||||
public void SetWebSocketAuth(ClientWebSocket socket)
|
||||
{
|
||||
if(_CachedAuth != null)
|
||||
if (_CachedAuth != null)
|
||||
socket.Options.SetRequestHeader("Authorization", $"{_CachedAuth.Scheme} {_CachedAuth.Parameter}");
|
||||
}
|
||||
}
|
||||
@ -74,33 +77,57 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
public ExplorerClient(NBXplorerNetwork network, Uri serverAddress = null)
|
||||
public ExplorerClient(NBXplorerNetwork network, Uri serverAddress = null) : this(network, serverAddress, null)
|
||||
{
|
||||
|
||||
}
|
||||
public ExplorerClient(NBXplorerNetwork network, Uri serverAddress, IAuth customAuth)
|
||||
{
|
||||
serverAddress = serverAddress ?? network.DefaultSettings.DefaultUrl;
|
||||
if(network == null)
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
_Address = serverAddress;
|
||||
_Network = network;
|
||||
_Serializer = new Serializer(network.NBitcoinNetwork);
|
||||
Serializer = new Serializer(network);
|
||||
_CryptoCode = _Network.CryptoCode;
|
||||
_Factory = new DerivationStrategy.DerivationStrategyFactory(Network.NBitcoinNetwork);
|
||||
SetCookieAuth(network.DefaultSettings.DefaultCookieFile);
|
||||
_Factory = Network.DerivationStrategyFactory;
|
||||
if (customAuth == null)
|
||||
{
|
||||
SetCookieAuth(network.DefaultSettings.DefaultCookieFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
_Auth = customAuth;
|
||||
customAuth.RefreshCache();
|
||||
}
|
||||
}
|
||||
|
||||
public RPCClient RPCClient { get; private set; }
|
||||
|
||||
internal IAuth _Auth = new NullAuthentication();
|
||||
|
||||
public bool SetCookieAuth(string path)
|
||||
{
|
||||
if(path == null)
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
CookieAuthentication auth = new CookieAuthentication(path);
|
||||
_Auth = auth;
|
||||
Auth = auth;
|
||||
return auth.RefreshCache();
|
||||
}
|
||||
|
||||
public void SetNoAuth()
|
||||
{
|
||||
_Auth = new NullAuthentication();
|
||||
Auth = new NullAuthentication();
|
||||
}
|
||||
|
||||
private void ConstructRPCClient()
|
||||
{
|
||||
RPCClient = new RPCClient(Auth is CookieAuthentication cookieAuthentication
|
||||
? new RPCCredentialString()
|
||||
{
|
||||
CookieFile = cookieAuthentication.CookieFilePath
|
||||
}
|
||||
: new RPCCredentialString(), GetFullUri($"v1/cryptos/{_CryptoCode}/rpc"), _Network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
private readonly string _CryptoCode = "BTC";
|
||||
@ -112,203 +139,479 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
Serializer _Serializer;
|
||||
DerivationStrategy.DerivationStrategyFactory _Factory;
|
||||
public UTXOChanges GetUTXOs(DerivationStrategyBase extKey, UTXOChanges previousChange, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
public UTXOChanges GetUTXOs(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUTXOsAsync(extKey, previousChange, longPolling, cancellation).GetAwaiter().GetResult();
|
||||
return GetUTXOsAsync(extKey, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<UTXOChanges> GetUTXOsAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
{
|
||||
if (extKey == null)
|
||||
throw new ArgumentNullException(nameof(extKey));
|
||||
return GetUTXOsAsync(TrackedSource.Create(extKey), cancellation);
|
||||
}
|
||||
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<TransactionResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/transactions/{txId}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return await SendAsync<TransactionResult>(HttpMethod.Get, null, "v1/cryptos/{0}/transactions/" + txId, new[] { CryptoCode }, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public TransactionResult GetTransaction(uint256 txId, CancellationToken cancellation = default(CancellationToken))
|
||||
public TransactionResult GetTransaction(uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionAsync(txId, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<UTXOChanges> GetUTXOsAsync(DerivationStrategyBase extKey, UTXOChanges previousChange, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<PruneResponse> PruneAsync(DerivationStrategyBase extKey, PruneRequest pruneRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUTXOsAsync(extKey, previousChange?.Confirmed?.Bookmark, previousChange?.Unconfirmed?.Bookmark, longPolling, cancellation);
|
||||
if (extKey == null)
|
||||
throw new ArgumentNullException(nameof(extKey));
|
||||
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/prune", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public UTXOChanges GetUTXOs(DerivationStrategyBase extKey, Bookmark confirmedBookmark, Bookmark unconfirmedBookmark, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUTXOsAsync(extKey, confirmedBookmark, unconfirmedBookmark, longPolling, cancellation).GetAwaiter().GetResult();
|
||||
return PruneAsync(extKey, pruneRequest, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public NotificationSession CreateNotificationSession(CancellationToken cancellation = default(CancellationToken))
|
||||
internal class RawStr
|
||||
{
|
||||
return CreateNotificationSessionAsync(cancellation).GetAwaiter().GetResult();
|
||||
private string str;
|
||||
public RawStr(string str)
|
||||
{
|
||||
this.str = str;
|
||||
}
|
||||
public override string ToString() => str;
|
||||
};
|
||||
internal static RawStr Raw(string str) => new RawStr(str);
|
||||
public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (extKey == null)
|
||||
throw new ArgumentNullException(nameof(extKey));
|
||||
List<string> args = new List<string>();
|
||||
if (batchSize != null)
|
||||
args.Add($"batchsize={batchSize.Value}");
|
||||
if (gapLimit != null)
|
||||
args.Add($"gaplimit={gapLimit.Value}");
|
||||
if (fromIndex != null)
|
||||
args.Add($"from={fromIndex.Value}");
|
||||
var argsString = string.Join("&", args.ToArray());
|
||||
if (argsString != string.Empty)
|
||||
argsString = $"?{argsString}";
|
||||
await SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
public void ScanUTXOSet(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
|
||||
{
|
||||
ScanUTXOSetAsync(extKey, batchSize, gapLimit, fromIndex, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<NotificationSession> CreateNotificationSessionAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
{
|
||||
var session = new NotificationSession(this);
|
||||
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public ScanUTXOInformation GetScanUTXOSetInformation(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetScanUTXOSetInformationAsync(extKey, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public LongPollingNotificationSession CreateLongPollingNotificationSession(long lastEventId = 0)
|
||||
{
|
||||
return new LongPollingNotificationSession(lastEventId, this);
|
||||
}
|
||||
|
||||
public WebsocketNotificationSession CreateWebsocketNotificationSession(CancellationToken cancellation = default)
|
||||
{
|
||||
return CreateWebsocketNotificationSessionAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<WebsocketNotificationSession> CreateWebsocketNotificationSessionAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
var session = new WebsocketNotificationSession(this);
|
||||
await session.ConnectAsync(cancellation).ConfigureAwait(false);
|
||||
return session;
|
||||
}
|
||||
public WebsocketNotificationSessionLegacy CreateWebsocketNotificationSessionLegacy(CancellationToken cancellation = default)
|
||||
{
|
||||
return CreateWebsocketNotificationSessionLegacyAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<WebsocketNotificationSessionLegacy> CreateWebsocketNotificationSessionLegacyAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
var session = new WebsocketNotificationSessionLegacy(this);
|
||||
await session.ConnectAsync(cancellation).ConfigureAwait(false);
|
||||
return session;
|
||||
}
|
||||
|
||||
public Task<UTXOChanges> GetUTXOsAsync(DerivationStrategyBase extKey, Bookmark confirmedBookmark, Bookmark unconfirmedBookmark, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
public UTXOChanges GetUTXOs(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUTXOsAsync(extKey,
|
||||
confirmedBookmark == null ? null as Bookmark[] : new Bookmark[] { confirmedBookmark },
|
||||
unconfirmedBookmark == null ? null as Bookmark[] : new Bookmark[] { unconfirmedBookmark }, longPolling, cancellation);
|
||||
return GetUTXOsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
return SendAsync<UTXOChanges>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/utxos", cancellation);
|
||||
}
|
||||
|
||||
public async Task<UTXOChanges> GetUTXOsAsync(DerivationStrategyBase extKey, Bookmark[] confirmedBookmarks, Bookmark[] unconfirmedBookmarks, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
if(confirmedBookmarks != null)
|
||||
parameters.Add("confirmedBookmarks", String.Join(",", confirmedBookmarks.Select(b => b.ToString())));
|
||||
if(unconfirmedBookmarks != null)
|
||||
parameters.Add("unconfirmedBookmarks", String.Join(",", unconfirmedBookmarks.Select(b => b.ToString())));
|
||||
parameters.Add("longPolling", longPolling.ToString());
|
||||
|
||||
var query = String.Join("&", parameters.Select(p => p.Key + "=" + p.Value).ToArray());
|
||||
return await SendAsync<UTXOChanges>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos?" + query, new object[] { CryptoCode, extKey.ToString() }, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void WaitServerStarted(CancellationToken cancellation = default(CancellationToken))
|
||||
public void WaitServerStarted(CancellationToken cancellation = default)
|
||||
{
|
||||
WaitServerStartedAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task WaitServerStartedAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task WaitServerStartedAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await GetStatusAsync(cancellation).ConfigureAwait(false);
|
||||
if(status.IsFullySynched)
|
||||
if (status.IsFullySynched)
|
||||
break;
|
||||
await Task.Delay(10);
|
||||
}
|
||||
catch(OperationCanceledException) { throw; }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { }
|
||||
cancellation.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Track(DerivationStrategyBase strategy, CancellationToken cancellation = default(CancellationToken))
|
||||
public void Track(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
{
|
||||
TrackAsync(strategy, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, strategy.ToString() }, cancellation);
|
||||
return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation);
|
||||
}
|
||||
|
||||
public void CancelReservation(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default(CancellationToken))
|
||||
public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
TrackAsync(strategy, trackDerivationRequest, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task TrackAsync(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, $"v1/cryptos/{CryptoCode}/derivations/{strategy}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Track(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
return SendAsync<string>(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation);
|
||||
}
|
||||
|
||||
private Exception UnSupported(TrackedSource trackedSource)
|
||||
{
|
||||
return new NotSupportedException($"Unsupported {trackedSource.GetType().Name}");
|
||||
}
|
||||
|
||||
public void CancelReservation(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
|
||||
{
|
||||
CancelReservationAsync(strategy, keyPaths, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default(CancellationToken))
|
||||
public GetBalanceResponse GetBalance(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, keyPaths, "v1/cryptos/{0}/derivations/{1}/addresses/cancelreservation", new[] { CryptoCode, strategy.ToString() }, cancellation);
|
||||
return GetBalanceAsync(userDerivationScheme, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation);
|
||||
}
|
||||
|
||||
public StatusResult GetStatus(CancellationToken cancellation = default(CancellationToken))
|
||||
|
||||
public GetBalanceResponse GetBalance(BitcoinAddress address, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetBalanceAsync(address, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetBalanceAsync(TrackedSource.Create(address), cancellation);
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation);
|
||||
}
|
||||
public async Task<bool> IsTrackedAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
|
||||
var responseMessage = await SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}", cancellation);
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.OK:
|
||||
return true;
|
||||
case HttpStatusCode.NotFound:
|
||||
return false;
|
||||
default:
|
||||
await ParseResponse(responseMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation);
|
||||
}
|
||||
|
||||
public StatusResult GetStatus(CancellationToken cancellation = default)
|
||||
{
|
||||
return GetStatusAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<StatusResult> GetStatusAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
public void Wipe(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", null, cancellation);
|
||||
WipeAsync(strategy, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, GetTransactionsResponse previous, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task WipeAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(strategy, previous, longPolling, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, Bookmark[] confirmedBookmarks, Bookmark[] unconfirmedBookmarks, Bookmark[] replacedBookmarks, bool longPolling = true, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return GetTransactionsAsync(strategy, confirmedBookmarks, unconfirmedBookmarks, replacedBookmarks, longPolling, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, GetTransactionsResponse previous, bool longPolling, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return GetTransactionsAsync(strategy,
|
||||
previous == null ? null : new[] { previous.ConfirmedTransactions.Bookmark },
|
||||
previous == null ? null : new[] { previous.UnconfirmedTransactions.Bookmark },
|
||||
previous == null ? null : new[] { previous.ReplacedTransactions.Bookmark }, longPolling, cancellation);
|
||||
}
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, Bookmark[] confirmedBookmarks, Bookmark[] unconfirmedBookmarks, Bookmark[] replacedBookmarks, bool longPolling, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
if(confirmedBookmarks != null)
|
||||
parameters.Add("confirmedBookmarks", String.Join(",", confirmedBookmarks.Select(b => b.ToString())));
|
||||
if(unconfirmedBookmarks != null)
|
||||
parameters.Add("unconfirmedBookmarks", String.Join(",", unconfirmedBookmarks.Select(b => b.ToString())));
|
||||
if(replacedBookmarks != null)
|
||||
parameters.Add("replacedBookmarks", String.Join(",", replacedBookmarks.Select(b => b.ToString())));
|
||||
parameters.Add("longPolling", longPolling.ToString());
|
||||
var query = String.Join("&", parameters.Select(p => p.Key + "=" + p.Value).ToArray());
|
||||
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/transactions?" + query, null, cancellation);
|
||||
if (strategy is null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
return SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/utxos/wipe", cancellation);
|
||||
}
|
||||
|
||||
public KeyPathInformation GetUnused(DerivationStrategyBase strategy, DerivationFeature feature, int skip = 0, bool reserve = false, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task<StatusResult> GetStatusAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", cancellation);
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(strategy, from, to, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(trackedSource, from, to, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(TrackedSource.Create(strategy), from, to, cancellation);
|
||||
}
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
string fromV = string.Empty;
|
||||
string toV = string.Empty;
|
||||
if (from is DateTimeOffset f)
|
||||
{
|
||||
fromV = NBitcoin.Utils.DateTimeToUnixTime(f).ToString();
|
||||
}
|
||||
if (to is DateTimeOffset t)
|
||||
{
|
||||
toV = NBitcoin.Utils.DateTimeToUnixTime(t).ToString();
|
||||
}
|
||||
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions?from={fromV}&to={toV}", cancellation);
|
||||
}
|
||||
|
||||
|
||||
public TransactionInformation GetTransaction(TrackedSource trackedSource, uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
return this.GetTransactionAsync(trackedSource, txId, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public TransactionInformation GetTransaction(DerivationStrategyBase derivationStrategyBase, uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
return this.GetTransactionAsync(derivationStrategyBase, txId, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<TransactionInformation> GetTransactionAsync(DerivationStrategyBase derivationStrategyBase, uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
if (derivationStrategyBase == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategyBase));
|
||||
return GetTransactionAsync(new DerivationSchemeTrackedSource(derivationStrategyBase), txId, cancellation);
|
||||
}
|
||||
|
||||
public Task<TransactionInformation> GetTransactionAsync(TrackedSource trackedSource, uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
if (txId == null)
|
||||
throw new ArgumentNullException(nameof(txId));
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation);
|
||||
}
|
||||
|
||||
public Task RescanAsync(RescanRequest rescanRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
if (rescanRequest == null)
|
||||
throw new ArgumentNullException(nameof(rescanRequest));
|
||||
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", cancellation);
|
||||
}
|
||||
|
||||
public void Rescan(RescanRequest rescanRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
RescanAsync(rescanRequest, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public KeyPathInformation GetUnused(DerivationStrategyBase strategy, DerivationFeature feature, int skip = 0, bool reserve = false, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUnusedAsync(strategy, feature, skip, reserve, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<KeyPathInformation> GetUnusedAsync(DerivationStrategyBase strategy, DerivationFeature feature, int skip = 0, bool reserve = false, CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<KeyPathInformation> GetUnusedAsync(DerivationStrategyBase strategy, DerivationFeature feature, int skip = 0, bool reserve = false, CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", null, cancellation).ConfigureAwait(false);
|
||||
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch(NBXplorerException ex) when(ex.Error?.HttpCode == 404)
|
||||
catch (NBXplorerException ex) when (ex.Error?.HttpCode == 404)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default(CancellationToken))
|
||||
|
||||
public KeyPathInformation GetKeyInformation(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, "v1/cryptos/{0}/scripts/" + script.ToHex(), new[] { CryptoCode }, cancellation).ConfigureAwait(false);
|
||||
return GetKeyInformationAsync(strategy, script, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<KeyPathInformation> GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await GetKeyInformationAsync(new DerivationSchemeTrackedSource(strategy), script, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<KeyPathInformation> GetKeyInformationAsync(TrackedSource trackedSource, Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public GetFeeRateResult GetFeeRate(int blockCount, CancellationToken cancellation = default(CancellationToken))
|
||||
public GetFeeRateResult GetFeeRate(int blockCount, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetFeeRateAsync(blockCount, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public GetFeeRateResult GetFeeRate(int blockCount, FeeRate fallbackFeeRate, CancellationToken cancellation = default(CancellationToken))
|
||||
public GetFeeRateResult GetFeeRate(int blockCount, FeeRate fallbackFeeRate, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetFeeRateAsync(blockCount, fallbackFeeRate, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, FeeRate fallbackFeeRate, CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, FeeRate fallbackFeeRate, CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation).ConfigureAwait(false);
|
||||
return await GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch(NBXplorerException ex) when (fallbackFeeRate != null && ex.Error.Code == "fee-estimation-unavailable")
|
||||
catch (NBXplorerException ex) when (fallbackFeeRate != null && ex.Error.Code == "fee-estimation-unavailable")
|
||||
{
|
||||
return new GetFeeRateResult() { BlockCount = blockCount, FeeRate = fallbackFeeRate };
|
||||
}
|
||||
}
|
||||
public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation);
|
||||
return GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation);
|
||||
}
|
||||
public CreatePSBTResponse CreatePSBT(DerivationStrategyBase derivationStrategy, CreatePSBTRequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
return CreatePSBTAsync(derivationStrategy, request, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<CreatePSBTResponse> CreatePSBTAsync(DerivationStrategyBase derivationStrategy, CreatePSBTRequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations/{derivationStrategy}/psbt/create", cancellation);
|
||||
}
|
||||
|
||||
public BroadcastResult Broadcast(Transaction tx, CancellationToken cancellation = default(CancellationToken))
|
||||
public UpdatePSBTResponse UpdatePSBT(UpdatePSBTRequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
return BroadcastAsync(tx, cancellation).GetAwaiter().GetResult();
|
||||
return UpdatePSBTAsync(request, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<UpdatePSBTResponse> UpdatePSBTAsync(UpdatePSBTRequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/psbt/update", cancellation);
|
||||
}
|
||||
public BroadcastResult Broadcast(Transaction tx, CancellationToken cancellation = default)
|
||||
{
|
||||
return Broadcast(tx, false, cancellation);
|
||||
}
|
||||
public BroadcastResult Broadcast(Transaction tx, bool testMempoolAccept, CancellationToken cancellation = default)
|
||||
{
|
||||
return BroadcastAsync(tx, testMempoolAccept, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<BroadcastResult> BroadcastAsync(Transaction tx, CancellationToken cancellation = default(CancellationToken))
|
||||
public Task<BroadcastResult> BroadcastAsync(Transaction tx, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), "v1/cryptos/{0}/transactions", new[] { CryptoCode }, cancellation);
|
||||
return BroadcastAsync(tx, false, cancellation);
|
||||
}
|
||||
|
||||
public Task<BroadcastResult> BroadcastAsync(Transaction tx, bool testMempoolAccept, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), $"v1/cryptos/{CryptoCode}/transactions?testMempoolAccept={testMempoolAccept}", cancellation);
|
||||
}
|
||||
|
||||
public TMetadata GetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetMetadataAsync<TMetadata>(derivationScheme, key, cancellationToken).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task<TMetadata> GetMetadataAsync<TMetadata>(DerivationStrategyBase derivationScheme, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (derivationScheme == null)
|
||||
throw new ArgumentNullException(nameof(derivationScheme));
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
return GetAsync<TMetadata>($"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
|
||||
}
|
||||
|
||||
public void SetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
SetMetadataAsync<TMetadata>(derivationScheme, key, value, cancellationToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task SetMetadataAsync<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, value, $"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<GenerateWalletResponse> GenerateWalletAsync(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
request ??= new GenerateWalletRequest();
|
||||
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations", cancellationToken);
|
||||
}
|
||||
|
||||
public GenerateWalletResponse GenerateWallet(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
request ??= new GenerateWalletRequest();
|
||||
return GenerateWalletAsync(request, cancellationToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GroupInformation> CreateGroupAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, null, $"v1/groups", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> GetGroupAsync(string groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Get, null, $"v1/groups/{groupId}", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> AddGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, children, $"v1/groups/{groupId}/children", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> RemoveGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Delete, children, $"v1/groups/{groupId}/children", cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddGroupAddressAsync(string cryptoCode, string groupId, string[] addresses, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, addresses, $"v1/cryptos/{cryptoCode}/groups/{groupId}/addresses", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ImportUTXOs(string cryptoCode, ImportUTXORequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
await SendAsync(HttpMethod.Post, request, $"v1/cryptos/{cryptoCode}/rescan-utxos", cancellation);
|
||||
}
|
||||
|
||||
private static readonly HttpClient SharedClient = new HttpClient();
|
||||
@ -342,80 +645,126 @@ namespace NBXplorer
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
public Serializer Serializer { get; private set; }
|
||||
|
||||
|
||||
internal string GetFullUri(string relativePath, params object[] parameters)
|
||||
internal IAuth Auth
|
||||
{
|
||||
relativePath = String.Format(relativePath, parameters ?? new object[0]);
|
||||
var uri = Address.AbsoluteUri;
|
||||
if(!uri.EndsWith("/", StringComparison.Ordinal))
|
||||
uri += "/";
|
||||
uri += relativePath;
|
||||
if(!IncludeTransaction)
|
||||
get => _Auth;
|
||||
set
|
||||
{
|
||||
if(uri.IndexOf('?') == -1)
|
||||
_Auth = value;
|
||||
ConstructRPCClient();
|
||||
}
|
||||
}
|
||||
|
||||
static FormattableString EncodeUrlParameters(FormattableString url)
|
||||
{
|
||||
return FormattableStringFactory.Create(
|
||||
url.Format,
|
||||
url.GetArguments()
|
||||
.Select(a =>
|
||||
a is RawStr ? a :
|
||||
a is FormattableString o ? EncodeUrlParameters(o) :
|
||||
Uri.EscapeDataString(a?.ToString() ?? ""))
|
||||
.ToArray());
|
||||
}
|
||||
internal string GetFullUri(FormattableString relativePath)
|
||||
{
|
||||
var uri = Address.AbsoluteUri;
|
||||
if (!uri.EndsWith("/", StringComparison.Ordinal))
|
||||
uri += "/";
|
||||
uri += EncodeUrlParameters(relativePath).ToString();
|
||||
if (!IncludeTransaction)
|
||||
{
|
||||
if (uri.IndexOf('?') == -1)
|
||||
uri += $"?includeTransaction=false";
|
||||
else
|
||||
uri += $"&includeTransaction=false";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
private Task<T> GetAsync<T>(string relativePath, object[] parameters, CancellationToken cancellation)
|
||||
private Task<T> GetAsync<T>(FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters, cancellation);
|
||||
return SendAsync<T>(HttpMethod.Get, null, relativePath, cancellation);
|
||||
}
|
||||
private async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, object[] parameters, CancellationToken cancellation)
|
||||
internal async Task<T> SendAsync<T>(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
HttpRequestMessage message = CreateMessage(method, body, relativePath, parameters);
|
||||
HttpRequestMessage message = CreateMessage(method, body, relativePath);
|
||||
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
if((int)result.StatusCode == 404)
|
||||
if ((int)result.StatusCode == 404)
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
if((int)result.StatusCode == 401)
|
||||
if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout)
|
||||
{
|
||||
if(_Auth.RefreshCache())
|
||||
throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException());
|
||||
}
|
||||
if ((int)result.StatusCode == 401)
|
||||
{
|
||||
if (Auth.RefreshCache())
|
||||
{
|
||||
message = CreateMessage(method, body, relativePath, parameters);
|
||||
result = await Client.SendAsync(message).ConfigureAwait(false);
|
||||
message = CreateMessage(method, body, relativePath);
|
||||
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return await ParseResponse<T>(result).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, string relativePath, object[] parameters)
|
||||
internal async Task<HttpResponseMessage> SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
var uri = GetFullUri(relativePath, parameters);
|
||||
var message = new HttpRequestMessage(method, uri);
|
||||
_Auth.SetAuthorization(message);
|
||||
if(body != null)
|
||||
{
|
||||
if(body is byte[])
|
||||
message.Content = new ByteArrayContent((byte[])body);
|
||||
else
|
||||
message.Content = new StringContent(_Serializer.ToString(body), Encoding.UTF8, "application/json");
|
||||
}
|
||||
var message = CreateMessage(method, body, relativePath);
|
||||
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
|
||||
if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout)
|
||||
{
|
||||
throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException());
|
||||
}
|
||||
if ((int)result.StatusCode == 401)
|
||||
{
|
||||
if (Auth.RefreshCache())
|
||||
{
|
||||
message = CreateMessage(method, body, relativePath);
|
||||
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, FormattableString relativePath)
|
||||
{
|
||||
var uri = GetFullUri(relativePath);
|
||||
var message = new HttpRequestMessage(method, uri);
|
||||
Auth.SetAuthorization(message);
|
||||
if (body != null)
|
||||
{
|
||||
if (body is byte[])
|
||||
{
|
||||
var content = new ByteArrayContent((byte[])body);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
message.Content = content;
|
||||
}
|
||||
else
|
||||
message.Content = new StringContent(Serializer.ToString(body), Encoding.UTF8, "application/json");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private async Task<T> ParseResponse<T>(HttpResponseMessage response)
|
||||
{
|
||||
using(response)
|
||||
using (response)
|
||||
{
|
||||
if(response.IsSuccessStatusCode)
|
||||
if(response.Content.Headers.ContentLength == 0)
|
||||
if (response.IsSuccessStatusCode)
|
||||
if (response.Content.Headers.ContentLength == 0)
|
||||
return default(T);
|
||||
else if(response.Content.Headers.ContentType.MediaType.Equals("application/json", StringComparison.Ordinal))
|
||||
return _Serializer.ToObject<T>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
else if(response.Content.Headers.ContentType.MediaType.Equals("application/octet-stream", StringComparison.Ordinal))
|
||||
else if (response.Content.Headers.ContentType.MediaType.Equals("application/json", StringComparison.Ordinal))
|
||||
return Serializer.ToObject<T>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
else if (response.Content.Headers.ContentType.MediaType.Equals("application/octet-stream", StringComparison.Ordinal))
|
||||
{
|
||||
return (T)(object)await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
}
|
||||
if(response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
|
||||
response.EnsureSuccessStatusCode();
|
||||
var error = _Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
if(error == null)
|
||||
var error = Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
if (error == null)
|
||||
response.EnsureSuccessStatusCode();
|
||||
throw error.AsException();
|
||||
}
|
||||
@ -423,17 +772,30 @@ namespace NBXplorer
|
||||
|
||||
private async Task ParseResponse(HttpResponseMessage response)
|
||||
{
|
||||
using(response)
|
||||
using (response)
|
||||
{
|
||||
if(response.IsSuccessStatusCode)
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
if(response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
|
||||
response.EnsureSuccessStatusCode();
|
||||
var error = _Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
if(error == null)
|
||||
var error = Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
if (error == null)
|
||||
response.EnsureSuccessStatusCode();
|
||||
throw error.AsException();
|
||||
}
|
||||
}
|
||||
|
||||
private FormattableString GetBasePath(TrackedSource trackedSource)
|
||||
{
|
||||
if (trackedSource is null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
return trackedSource switch
|
||||
{
|
||||
DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}",
|
||||
AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}",
|
||||
GroupTrackedSource wts => $"v1/cryptos/{CryptoCode}/groups/{wts.GroupId}",
|
||||
_ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.RPC;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -12,84 +10,34 @@ namespace NBXplorer
|
||||
{
|
||||
public static class ExtensionsClient
|
||||
{
|
||||
static ExtensionsClient()
|
||||
public static IEnumerable<IList<T>> Batch<T>(this IEnumerable<T> values, int size)
|
||||
{
|
||||
_TypeByName = new Dictionary<string, Type>();
|
||||
_NameByType = new Dictionary<Type, string>();
|
||||
Add("newblock", typeof(Models.NewBlockEvent));
|
||||
Add("subscribeblock", typeof(Models.NewBlockEventRequest));
|
||||
Add("subscribetransaction", typeof(Models.NewTransactionEventRequest));
|
||||
Add("newtransaction", typeof(Models.NewTransactionEvent));
|
||||
}
|
||||
|
||||
static Dictionary<string, Type> _TypeByName;
|
||||
static Dictionary<Type, string> _NameByType;
|
||||
private static void Add(string typeName, Type type)
|
||||
{
|
||||
_TypeByName.Add(typeName, type);
|
||||
_NameByType.Add(type, typeName);
|
||||
}
|
||||
|
||||
public static IEnumerable<T[]> Batch<T>(this IEnumerable<T> values, int size)
|
||||
{
|
||||
var batch = new T[size];
|
||||
int index = 0;
|
||||
if (size <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(size));
|
||||
if (values == null)
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
if (values is IList<T> l && l.Count <= size)
|
||||
{
|
||||
yield return l;
|
||||
yield break;
|
||||
}
|
||||
var batch = new List<T>();
|
||||
foreach(var v in values)
|
||||
{
|
||||
batch[index++] = v;
|
||||
if(index == batch.Length)
|
||||
batch.Add(v);
|
||||
if(size == batch.Count)
|
||||
{
|
||||
yield return batch;
|
||||
batch = new T[size];
|
||||
index = 0;
|
||||
batch = new List<T>();
|
||||
}
|
||||
}
|
||||
if(index != 0)
|
||||
if(batch.Count != 0)
|
||||
{
|
||||
Array.Resize(ref batch, index);
|
||||
yield return batch;
|
||||
}
|
||||
}
|
||||
|
||||
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index)
|
||||
{
|
||||
|
||||
if((uint)index > (uint)array.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return new ArraySegment<T>(array.Array, array.Offset + index, array.Count - index);
|
||||
}
|
||||
|
||||
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index, int count)
|
||||
{
|
||||
if((uint)index > (uint)array.Count || (uint)count > (uint)(array.Count - index))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return new ArraySegment<T>(array.Array, array.Offset + index, count);
|
||||
}
|
||||
|
||||
public static object ParseNotificationMessage(string str, JsonSerializerSettings settings)
|
||||
{
|
||||
if(str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
JObject jobj = JObject.Parse(str);
|
||||
var type = (jobj["type"] as JValue)?.Value<string>();
|
||||
if(type == null)
|
||||
throw new FormatException("'type' property not found");
|
||||
if(!_TypeByName.TryGetValue(type, out Type typeObject))
|
||||
throw new FormatException("unknown 'type'");
|
||||
var data = (jobj["data"] as JObject);
|
||||
if(data == null)
|
||||
throw new FormatException("'data' property not found");
|
||||
|
||||
return JsonConvert.DeserializeObject(data.ToString(), typeObject, settings);
|
||||
}
|
||||
|
||||
public static async Task CloseSocket(this WebSocket socket, WebSocketCloseStatus status, string statusDescription, CancellationToken cancellation = default(CancellationToken))
|
||||
public static async Task CloseSocket(this WebSocket socket, WebSocketCloseStatus status, string statusDescription, CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -113,10 +61,18 @@ namespace NBXplorer
|
||||
finally { socket.Dispose(); }
|
||||
}
|
||||
|
||||
public static string GetNotificationMessageTypeName(Type type)
|
||||
public static async Task<uint256[]> EnsureGenerateAsync(this RPCClient client, int blockCount)
|
||||
{
|
||||
_NameByType.TryGetValue(type, out string name);
|
||||
return name;
|
||||
uint256[] blockIds = new uint256[blockCount];
|
||||
int generated = 0;
|
||||
while(generated < blockCount)
|
||||
{
|
||||
foreach(var id in await client.GenerateAsync(blockCount - generated).ConfigureAwait(false))
|
||||
{
|
||||
blockIds[generated++] = id;
|
||||
}
|
||||
}
|
||||
return blockIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class BookmarkJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return
|
||||
typeof(Bookmark).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if(reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
return new Bookmark(new uint160(reader.Value.ToString()));
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var b = value as Bookmark;
|
||||
if(b != null)
|
||||
{
|
||||
writer.WriteValue(b.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
NBXplorer.Client/JsonConverters/CachedSerializer.cs
Normal file
88
NBXplorer.Client/JsonConverters/CachedSerializer.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Cache serialization of costly base58 structures
|
||||
/// </summary>
|
||||
public class CachedSerializer : JsonConverter
|
||||
{
|
||||
class CachedConverter
|
||||
{
|
||||
public CachedConverter(JsonConverter converter)
|
||||
{
|
||||
Converter = converter;
|
||||
}
|
||||
|
||||
public JsonConverter Converter { get; }
|
||||
ConcurrentDictionary<string, object> cachedStrings = new ConcurrentDictionary<string, object>();
|
||||
int total = 0;
|
||||
public object ReadJson(string str, JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (cachedStrings.TryGetValue(str, out var v))
|
||||
return v;
|
||||
v = Converter.ReadJson(reader, objectType, existingValue, serializer);
|
||||
if (cachedStrings.TryAdd(str, v))
|
||||
{
|
||||
Interlocked.Increment(ref total);
|
||||
if (total > 20)
|
||||
{
|
||||
cachedStrings.Clear();
|
||||
total = 0;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
public void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
Converter.WriteJson(writer, value, serializer);
|
||||
}
|
||||
|
||||
internal bool CanConvert(Type objectType)
|
||||
{
|
||||
return Converter.CanConvert(objectType);
|
||||
}
|
||||
}
|
||||
public CachedSerializer(NBXplorerNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
cachedConverter.Add(new CachedConverter(new DerivationStrategyJsonConverter(network.DerivationStrategyFactory)));
|
||||
cachedConverter.Add(new CachedConverter(new TrackedSourceJsonConverter(network)));
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return GetConverter(objectType) != null;
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
var str = reader.Value.ToString();
|
||||
return GetConverter(objectType).ReadJson(str, reader, objectType, existingValue, serializer);
|
||||
}
|
||||
|
||||
private CachedConverter GetConverter(Type objectType)
|
||||
{
|
||||
return cachedConverter.FirstOrDefault(s => s.CanConvert(objectType));
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
var converter = GetConverter(value.GetType());
|
||||
converter.WriteJson(writer, value, serializer);
|
||||
}
|
||||
|
||||
List<CachedConverter> cachedConverter = new List<CachedConverter>();
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,7 @@
|
||||
using NBitcoin;
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Reflection;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class FeeRateJsonConverter : JsonConverter
|
||||
{
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return
|
||||
typeof(FeeRate).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if(reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if(reader.TokenType != JsonToken.Integer)
|
||||
return null;
|
||||
|
||||
var value = (long)reader.Value;
|
||||
return new FeeRate(Money.Satoshis(value), 1);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var feeRate = value as FeeRate;
|
||||
if(feeRate != null)
|
||||
{
|
||||
writer.WriteValue(feeRate.GetFee(1).Satoshi);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class KeyPathTemplateJsonConverter : JsonConverter<KeyPathTemplate>
|
||||
{
|
||||
public override KeyPathTemplate ReadJson(JsonReader reader, Type objectType, KeyPathTemplate existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Unexpected json token type, expected String, actual {reader.TokenType}", reader);
|
||||
if (!KeyPathTemplate.TryParse((string)reader.Value, out var template))
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Invalid KeyPathTemplate", reader);
|
||||
return template;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, KeyPathTemplate value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is KeyPathTemplate kt)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
76
NBXplorer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
76
NBXplorer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class MoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(IMoney).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
class AssetCoinJson
|
||||
{
|
||||
public uint256 AssetId { get; set; }
|
||||
public long? Value { get; set; }
|
||||
public AssetMoney ToAssetMoney(string path)
|
||||
{
|
||||
if (AssetId == null)
|
||||
throw new JsonObjectException("'assetId' is missing", path);
|
||||
if (Value is null)
|
||||
throw new JsonObjectException("'value' is missing", path);
|
||||
return new AssetMoney(AssetId, Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
AssertJsonType(reader, new[] { JsonToken.Integer, JsonToken.StartObject, JsonToken.StartArray });
|
||||
if (reader.TokenType == JsonToken.Integer)
|
||||
{
|
||||
return new Money((long)reader.Value);
|
||||
}
|
||||
else if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
return serializer.Deserialize<AssetCoinJson>(reader).ToAssetMoney(reader.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MoneyBag(serializer.Deserialize<AssetCoinJson[]>(reader).Select(c => c.ToAssetMoney(reader.Path)).ToArray());
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in satoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is Money v)
|
||||
writer.WriteValue(v.Satoshi);
|
||||
else if (value is AssetMoney av)
|
||||
{
|
||||
serializer.Serialize(writer, new AssetCoinJson() { Value = av.Quantity, AssetId = av.AssetId });
|
||||
}
|
||||
else if (value is MoneyBag mb)
|
||||
{
|
||||
serializer.Serialize(writer, mb.OfType<AssetMoney>().Select(av2 => new AssetCoinJson() { Value = av2.Quantity, AssetId = av2.AssetId }).ToArray());
|
||||
}
|
||||
}
|
||||
static void AssertJsonType(JsonReader reader, JsonToken[] anyExpectedTypes)
|
||||
{
|
||||
if (!anyExpectedTypes.Contains(reader.TokenType))
|
||||
throw new JsonObjectException($"Unexpected json token type, expected are {string.Join(", ", anyExpectedTypes)} and actual is {reader.TokenType}", reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class PSBTDestinationJsonConverter : JsonConverter
|
||||
{
|
||||
public PSBTDestinationJsonConverter(Network network)
|
||||
{
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Network Network { get; }
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(PSBTDestination).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
{
|
||||
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
|
||||
}
|
||||
var str = reader.Value.ToString();
|
||||
try
|
||||
{
|
||||
return PSBTDestination.Parse(str, Network);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new JsonObjectException(ex.Message, reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
NBXplorer.Client/JsonConverters/ScriptPubKeyTypeConverter.cs
Normal file
50
NBXplorer.Client/JsonConverters/ScriptPubKeyTypeConverter.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class ScriptPubKeyTypeConverter : JsonConverter
|
||||
{
|
||||
static ScriptPubKeyTypeConverter()
|
||||
{
|
||||
_ScriptPubKeyType = new Dictionary<string, ScriptPubKeyType>()
|
||||
{
|
||||
{ "Legacy", ScriptPubKeyType.Legacy },
|
||||
{ "Segwit", ScriptPubKeyType.Segwit },
|
||||
{ "SegwitP2SH", ScriptPubKeyType.SegwitP2SH },
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
{ "Taproot", ScriptPubKeyType.TaprootBIP86 }
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
};
|
||||
_ScriptPubKeyTypeReverse = _ScriptPubKeyType.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(NBitcoin.WordCount).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Unexpected json token type, expected String, actual {reader.TokenType}", reader);
|
||||
if (!_ScriptPubKeyType.TryGetValue((string)reader.Value, out var result))
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Invalid ScriptPubKeyType, possible values {string.Join(", ", _ScriptPubKeyType.Keys.ToArray())} (defaut: 12)", reader);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is ScriptPubKeyType t)
|
||||
writer.WriteValue(_ScriptPubKeyTypeReverse[t]);
|
||||
}
|
||||
|
||||
readonly static Dictionary<string, ScriptPubKeyType> _ScriptPubKeyType;
|
||||
readonly static Dictionary<ScriptPubKeyType, string> _ScriptPubKeyTypeReverse;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using NBXplorer.Models;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class TrackedSourceJsonConverter : JsonConverter
|
||||
{
|
||||
public TrackedSourceJsonConverter(NBXplorerNetwork network)
|
||||
{
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public NBXplorerNetwork Network { get; }
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return
|
||||
typeof(TrackedSource).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
return null;
|
||||
|
||||
if (TrackedSource.TryParse(reader.Value.ToString(), out var v, Network))
|
||||
return v;
|
||||
throw new JsonObjectException("Invalid TrackedSource", reader);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var trackedSource = value as TrackedSource;
|
||||
if (trackedSource != null)
|
||||
{
|
||||
writer.WriteValue(trackedSource.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
57
NBXplorer.Client/JsonConverters/WordcountJsonConverter.cs
Normal file
57
NBXplorer.Client/JsonConverters/WordcountJsonConverter.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class WordcountJsonConverter : JsonConverter
|
||||
{
|
||||
static WordcountJsonConverter()
|
||||
{
|
||||
_Wordcount = new Dictionary<long, WordCount>()
|
||||
{
|
||||
{ 18, WordCount.Eighteen },
|
||||
{ 15, WordCount.Fifteen },
|
||||
{ 12, WordCount.Twelve },
|
||||
{ 24, WordCount.TwentyFour },
|
||||
{ 21, WordCount.TwentyOne }
|
||||
};
|
||||
_WordcountReverse = _Wordcount.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(NBitcoin.WordCount).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) ||
|
||||
typeof(NBitcoin.WordCount?).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return default;
|
||||
if (reader.TokenType != JsonToken.Integer)
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Unexpected json token type, expected Integer, actual {reader.TokenType}", reader);
|
||||
if (!_Wordcount.TryGetValue((long)reader.Value, out var result))
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Invalid WordCount, possible values {string.Join(", ", _Wordcount.Keys.ToArray())} (defaut: 12)", reader);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is WordCount wc)
|
||||
writer.WriteValue(_WordcountReverse[wc]);
|
||||
}
|
||||
|
||||
readonly static Dictionary<long, WordCount> _Wordcount = new Dictionary<long, WordCount>()
|
||||
{
|
||||
{ 18, WordCount.Eighteen },
|
||||
{ 15, WordCount.Fifteen },
|
||||
{ 12, WordCount.Twelve },
|
||||
{ 24, WordCount.TwentyFour },
|
||||
{ 21, WordCount.TwentyOne }
|
||||
};
|
||||
readonly static Dictionary<WordCount, long> _WordcountReverse;
|
||||
}
|
||||
}
|
||||
49
NBXplorer.Client/JsonConverters/WordlistJsonConverter.cs
Normal file
49
NBXplorer.Client/JsonConverters/WordlistJsonConverter.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class WordlistJsonConverter : JsonConverter
|
||||
{
|
||||
static WordlistJsonConverter()
|
||||
{
|
||||
_Wordlists = new Dictionary<string, Wordlist>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "English", Wordlist.English },
|
||||
{ "French", Wordlist.French },
|
||||
{ "Japanese", Wordlist.Japanese },
|
||||
{ "Spanish", Wordlist.Spanish },
|
||||
{ "ChineseSimplified", Wordlist.ChineseSimplified }
|
||||
};
|
||||
_WordlistsReverse = _Wordlists.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(Wordlist).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Unexpected json token type, expected String, actual {reader.TokenType}", reader);
|
||||
if (!_Wordlists.TryGetValue((string)reader.Value, out var result))
|
||||
throw new NBitcoin.JsonConverters.JsonObjectException($"Invalid wordlist, possible values {string.Join(", ", _Wordlists.Keys.ToArray())} (defaut: English)", reader);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is Wordlist wl)
|
||||
writer.WriteValue(_WordlistsReverse[wl]);
|
||||
}
|
||||
|
||||
readonly static Dictionary<string, Wordlist> _Wordlists;
|
||||
readonly static Dictionary<Wordlist, string> _WordlistsReverse;
|
||||
}
|
||||
}
|
||||
105
NBXplorer.Client/LongPollingNotificationSession.cs
Normal file
105
NBXplorer.Client/LongPollingNotificationSession.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class LongPollingNotificationSession : NotificationSessionBase
|
||||
{
|
||||
public LongPollingNotificationSession(long lastEventId, ExplorerClient client)
|
||||
{
|
||||
LastEventId = lastEventId;
|
||||
Client = client;
|
||||
}
|
||||
|
||||
public long LastEventId { get; private set; }
|
||||
public ExplorerClient Client { get; }
|
||||
Queue<NewEventBase> _EventsToProcess = new Queue<NewEventBase>();
|
||||
|
||||
public override async Task<NewEventBase> NextEventAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
retry:
|
||||
long evtId = 0;
|
||||
lock (_EventsToProcess)
|
||||
{
|
||||
evtId = LastEventId;
|
||||
if (_EventsToProcess.Count > 0)
|
||||
{
|
||||
var evt = _EventsToProcess.Dequeue();
|
||||
LastEventId = evt.EventId;
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
NewEventBase[] evts = null;
|
||||
try
|
||||
{
|
||||
evts = await GetEventsAsync(evtId, 30, true, cancellation);
|
||||
}
|
||||
catch(HttpRequestException ex) when (ex.InnerException is TimeoutException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
catch(OperationCanceledException) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
lock (_EventsToProcess)
|
||||
{
|
||||
if (_EventsToProcess.Count != 0)
|
||||
goto retry;
|
||||
foreach (var item in evts)
|
||||
{
|
||||
_EventsToProcess.Enqueue(item);
|
||||
}
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
|
||||
public NewEventBase[] GetEvents(long lastEventId = 0, int? limit = null, bool longPolling = false, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetEventsAsync(lastEventId, limit, longPolling, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<NewEventBase[]> GetEventsAsync(long lastEventId = 0, int? limit = null, bool longPolling = false, CancellationToken cancellation = default)
|
||||
{
|
||||
List<string> parameters = new List<string>();
|
||||
if (lastEventId != 0)
|
||||
parameters.Add($"lastEventId={lastEventId}");
|
||||
if (limit != null)
|
||||
parameters.Add($"limit={limit.Value}");
|
||||
if (longPolling)
|
||||
parameters.Add($"longPolling={longPolling}");
|
||||
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{ExplorerClient.Raw(parametersString)}", cancellation);
|
||||
|
||||
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
|
||||
.OfType<NewEventBase>()
|
||||
.ToArray();
|
||||
return evtsObj;
|
||||
}
|
||||
|
||||
public NewEventBase[] GetLatestEvents(int limit = 10, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetLatestEventsAsync(limit, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<NewEventBase[]> GetLatestEventsAsync(int limit = 10, CancellationToken cancellation = default)
|
||||
{
|
||||
List<string> parameters = new List<string>();
|
||||
if (limit != 10)
|
||||
parameters.Add($"limit={limit}");
|
||||
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{ExplorerClient.Raw(parametersString)}", cancellation);
|
||||
|
||||
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
|
||||
.OfType<NewEventBase>()
|
||||
.ToArray();
|
||||
return evtsObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class Bookmark
|
||||
{
|
||||
public Bookmark(uint160 value)
|
||||
{
|
||||
if(value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
_Value = value;
|
||||
}
|
||||
|
||||
private uint160 _Value;
|
||||
|
||||
private static readonly Bookmark _Start = new Bookmark(uint160.Zero);
|
||||
public static Bookmark Start
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
Bookmark item = obj as Bookmark;
|
||||
if(item == null)
|
||||
return false;
|
||||
return _Value.Equals(item._Value);
|
||||
}
|
||||
public static bool operator ==(Bookmark a, Bookmark b)
|
||||
{
|
||||
if(System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if(((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a._Value == b._Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(Bookmark a, Bookmark b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _Value.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin.RPC;
|
||||
using NBitcoin.RPC;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
|
||||
206
NBXplorer.Client/Models/CreatePSBTRequest.cs
Normal file
206
NBXplorer.Client/Models/CreatePSBTRequest.cs
Normal file
@ -0,0 +1,206 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class CreatePSBTRequest
|
||||
{
|
||||
[JsonProperty("PSBTVersion")]
|
||||
public int? PSBTVersion { get; set; }
|
||||
/// <summary>
|
||||
/// A seed to specific to get a deterministic PSBT (useful for tests)
|
||||
/// </summary>
|
||||
public int? Seed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the transaction (Optional, default to 1)
|
||||
/// </summary>
|
||||
public uint? Version { get; set; }
|
||||
/// <summary>
|
||||
/// The timelock of the transaction, activate RBF if not null (Optional: null, nLockTime to 0)
|
||||
/// </summary>
|
||||
public LockTime? LockTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Discourage fee sniping (Default: true)
|
||||
/// </summary>
|
||||
public bool? DiscourageFeeSniping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the include the global xpub in the PSBT (default: false)
|
||||
/// </summary>
|
||||
public bool? IncludeGlobalXPub { get; set; }
|
||||
/// <summary>
|
||||
/// Whether this transaction should use RBF or not.
|
||||
/// </summary>
|
||||
public bool? RBF { get; set; }
|
||||
/// <summary>
|
||||
/// Whether this transaction should merge the outputs.
|
||||
/// </summary>
|
||||
public bool? MergeOutputs { get; set; }
|
||||
/// <summary>
|
||||
/// The destinations where to send the money
|
||||
/// </summary>
|
||||
public List<CreatePSBTDestination> Destinations { get; set; } = new List<CreatePSBTDestination>();
|
||||
/// <summary>
|
||||
/// Fee settings
|
||||
/// </summary>
|
||||
public FeePreference FeePreference { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the creation of this PSBT will reserve a new change address
|
||||
/// </summary>
|
||||
public bool ReserveChangeAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default to 0, the minimum confirmations a UTXO need to be selected. (by default unconfirmed and confirmed UTXO will be used)
|
||||
/// </summary>
|
||||
public int MinConfirmations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Do not select the following outpoints for creating the PSBT (default to empty)
|
||||
/// </summary>
|
||||
public List<OutPoint> ExcludeOutpoints { get; set; }
|
||||
/// <summary>
|
||||
/// Only select the following outpoints for creating the PSBT (default to null)
|
||||
/// </summary>
|
||||
public List<OutPoint> IncludeOnlyOutpoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If `true`, all the UTXOs that have been selected will be used as input in the PSBT. (default to false)
|
||||
/// </summary>
|
||||
public bool? SpendAllMatchingOutpoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Use a specific change address (Optional, default: null, mutually exclusive with ReserveChangeAddress)
|
||||
/// </summary>
|
||||
public PSBTDestination ExplicitChangeAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rebase the hdkey paths (if no rebase, the key paths are relative to the xpub that NBXplorer knows about)
|
||||
/// This transform (PubKey0, 0/0, accountFingerprint) by (PubKey0, m/49'/0'/0/0, masterFingerprint)
|
||||
/// </summary>
|
||||
public List<PSBTRebaseKeyRules> RebaseKeyPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Under this value, UTXO's will be ignored.
|
||||
/// </summary>
|
||||
|
||||
public Money MinValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disabling the randomization of unspecified parameters to match the network's fingerprint distribution
|
||||
/// </summary>
|
||||
public bool? DisableFingerprintRandomization { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempt setting non_witness_utxo for all inputs even if they are segwit.
|
||||
/// </summary>
|
||||
public bool AlwaysIncludeNonWitnessUTXO { get; set; }
|
||||
}
|
||||
public class PSBTRebaseKeyRules
|
||||
{
|
||||
/// <summary>
|
||||
/// The account key to rebase
|
||||
/// </summary>
|
||||
public BitcoinExtPubKey AccountKey { get; set; }
|
||||
/// <summary>
|
||||
/// The path from the root to the account key
|
||||
/// </summary>
|
||||
public RootedKeyPath AccountKeyPath { get; set; }
|
||||
}
|
||||
public class ScriptDestination : IDestination
|
||||
{
|
||||
public ScriptDestination(Script scriptPubKey)
|
||||
{
|
||||
ScriptPubKey = scriptPubKey;
|
||||
}
|
||||
public Script ScriptPubKey { get; }
|
||||
}
|
||||
|
||||
public abstract class PSBTDestination
|
||||
{
|
||||
public static implicit operator PSBTDestination(Script script) => new ScriptType(script);
|
||||
public static implicit operator PSBTDestination(BitcoinAddress address) => new AddressType(address);
|
||||
public class ScriptType : PSBTDestination
|
||||
{
|
||||
public ScriptType(Script scriptPubKey)
|
||||
{
|
||||
if (scriptPubKey is null)
|
||||
throw new ArgumentNullException(nameof(scriptPubKey));
|
||||
ScriptPubKey = scriptPubKey;
|
||||
}
|
||||
public override Script ScriptPubKey { get; }
|
||||
public override string ToString() => ScriptPubKey.ToHex();
|
||||
}
|
||||
public class AddressType : PSBTDestination
|
||||
{
|
||||
public AddressType(BitcoinAddress address)
|
||||
{
|
||||
if (address is null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
Address = address;
|
||||
}
|
||||
public BitcoinAddress Address { get; }
|
||||
public override Script ScriptPubKey => Address.ScriptPubKey;
|
||||
public override string ToString() => Address.ToString();
|
||||
}
|
||||
public abstract Script ScriptPubKey { get; }
|
||||
public static PSBTDestination Create(Script script) => new ScriptType(script);
|
||||
public static PSBTDestination Create(BitcoinAddress address) => new AddressType(address);
|
||||
|
||||
public static PSBTDestination Parse(string str, Network network)
|
||||
{
|
||||
if (str is null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
if (network is null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (HexEncoder.IsWellFormed(str))
|
||||
return new ScriptType(Script.FromHex(str));
|
||||
else
|
||||
{
|
||||
return new AddressType(BitcoinAddress.Create(str, network));
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CreatePSBTDestination
|
||||
{
|
||||
/// <summary>
|
||||
/// The destination as an address or a script. (in hex)
|
||||
/// </summary>
|
||||
public PSBTDestination Destination { get; set; }
|
||||
/// <summary>
|
||||
/// Will Send this amount to this destination (Mutually exclusive with: SweepAll)
|
||||
/// </summary>
|
||||
public Money Amount { get; set; }
|
||||
/// <summary>
|
||||
/// Will substract the fees of this transaction to this destination (Mutually exclusive with: SweepAll)
|
||||
/// </summary>
|
||||
public bool SubstractFees { get; set; }
|
||||
/// <summary>
|
||||
/// Will sweep all the balance of your wallet to this destination (Mutually exclusive with: Amount, SubstractFees)
|
||||
/// </summary>
|
||||
public bool SweepAll { get; set; }
|
||||
}
|
||||
public class FeePreference
|
||||
{
|
||||
/// <summary>
|
||||
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, FallbackFeeRate)
|
||||
/// </summary>
|
||||
public FeeRate ExplicitFeeRate { get; set; }
|
||||
/// <summary>
|
||||
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, FallbackFeeRate)
|
||||
/// </summary>
|
||||
public Money ExplicitFee { get; set; }
|
||||
/// <summary>
|
||||
/// A number of blocks after which the user expect one confirmation (Mutually exclusive with: ExplicitFeeRate, ExplicitFee)
|
||||
/// </summary>
|
||||
public int? BlockTarget { get; set; }
|
||||
/// <summary>
|
||||
/// If the NBXplorer's node does not have proper fee estimation, this specific rate will be use in Satoshi per vBytes. (Mutually exclusive with: ExplicitFeeRate, ExplicitFee)
|
||||
/// </summary>
|
||||
public FeeRate FallbackFeeRate { get; set; }
|
||||
}
|
||||
}
|
||||
18
NBXplorer.Client/Models/CreatePSBTResponse.cs
Normal file
18
NBXplorer.Client/Models/CreatePSBTResponse.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class CreatePSBTResponse
|
||||
{
|
||||
[JsonProperty("psbt")]
|
||||
public PSBT PSBT { get; set; }
|
||||
public BitcoinAddress ChangeAddress { get; set; }
|
||||
public CreatePSBTSuggestions Suggestions { get; set; }
|
||||
|
||||
}
|
||||
public class CreatePSBTSuggestions
|
||||
{
|
||||
public bool ShouldEnforceLowR { get; set; }
|
||||
}
|
||||
}
|
||||
23
NBXplorer.Client/Models/GenerateWalletRequest.cs
Normal file
23
NBXplorer.Client/Models/GenerateWalletRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GenerateWalletRequest
|
||||
{
|
||||
public int AccountNumber { get; set; }
|
||||
public string ExistingMnemonic { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.WordlistJsonConverter))]
|
||||
public NBitcoin.Wordlist WordList { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.WordcountJsonConverter))]
|
||||
public NBitcoin.WordCount? WordCount { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.ScriptPubKeyTypeConverter))]
|
||||
public NBitcoin.ScriptPubKeyType? ScriptPubKeyType { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
[Obsolete("We will remove this feature in a future release.")]
|
||||
public bool ImportKeysToRPC { get; set; }
|
||||
public bool SavePrivateKeys { get; set; }
|
||||
public Dictionary<string, string> AdditionalOptions { get; set; }
|
||||
}
|
||||
}
|
||||
28
NBXplorer.Client/Models/GenerateWalletResponse.cs
Normal file
28
NBXplorer.Client/Models/GenerateWalletResponse.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GenerateWalletResponse
|
||||
{
|
||||
public string TrackedSource { get; set; }
|
||||
public string Mnemonic { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.WordlistJsonConverter))]
|
||||
public NBitcoin.Wordlist WordList { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.WordcountJsonConverter))]
|
||||
public NBitcoin.WordCount WordCount { get; set; }
|
||||
public BitcoinExtKey MasterHDKey { get; set; }
|
||||
public BitcoinExtKey AccountHDKey { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
|
||||
public NBitcoin.RootedKeyPath AccountKeyPath { get; set; }
|
||||
public string AccountDescriptor { get; set; }
|
||||
public StandardDerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
public Mnemonic GetMnemonic()
|
||||
{
|
||||
return new Mnemonic(Mnemonic, WordList);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
NBXplorer.Client/Models/GetBalanceResponse.cs
Normal file
29
NBXplorer.Client/Models/GetBalanceResponse.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GetBalanceResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// How the confirmed balance would be updated once all the unconfirmed transactions were confirmed.
|
||||
/// </summary>
|
||||
public IMoney Unconfirmed { get; set; }
|
||||
/// <summary>
|
||||
/// The balance of all funds in confirmed transactions.
|
||||
/// </summary>
|
||||
public IMoney Confirmed { get; set; }
|
||||
/// <summary>
|
||||
/// The total of funds owned (ie, `confirmed + unconfirmed`)
|
||||
/// </summary>
|
||||
public IMoney Total { get; set; }
|
||||
/// <summary>
|
||||
/// The total unspendable funds (ie, coinbase reward which need 100 confirmations before being spendable)
|
||||
/// </summary>
|
||||
public IMoney Immature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total spendable balance. (ie, `total - immature`)
|
||||
/// </summary>
|
||||
public IMoney Available { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
|
||||
@ -2,22 +2,16 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GetTransactionsResponse
|
||||
{
|
||||
public class GetTransactionsResponse
|
||||
{
|
||||
public int Height
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool HasChanges()
|
||||
{
|
||||
return ConfirmedTransactions.HasChanges() || UnconfirmedTransactions.HasChanges() || ReplacedTransactions.HasChanges();
|
||||
}
|
||||
|
||||
public TransactionInformationSet ConfirmedTransactions
|
||||
{
|
||||
get; set;
|
||||
@ -28,6 +22,11 @@ namespace NBXplorer.Models
|
||||
get; set;
|
||||
} = new TransactionInformationSet();
|
||||
|
||||
public TransactionInformationSet ImmatureTransactions
|
||||
{
|
||||
get; set;
|
||||
} = new TransactionInformationSet();
|
||||
|
||||
public TransactionInformationSet ReplacedTransactions
|
||||
{
|
||||
get; set;
|
||||
@ -36,27 +35,15 @@ namespace NBXplorer.Models
|
||||
|
||||
public class TransactionInformationSet
|
||||
{
|
||||
public Bookmark KnownBookmark
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Bookmark Bookmark
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<TransactionInformation> Transactions
|
||||
{
|
||||
get; set;
|
||||
} = new List<TransactionInformation>();
|
||||
public bool HasChanges()
|
||||
{
|
||||
return KnownBookmark != Bookmark;
|
||||
}
|
||||
}
|
||||
|
||||
public class TransactionInformationMatch
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPath KeyPath
|
||||
{
|
||||
get; set;
|
||||
@ -76,11 +63,16 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Confirmations
|
||||
public long Confirmations
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int? Height
|
||||
public long? Height
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool IsMature
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -92,24 +84,28 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<TransactionInformationMatch> Outputs
|
||||
public List<MatchedOutput> Outputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<TransactionInformationMatch>();
|
||||
} = new List<MatchedOutput>();
|
||||
|
||||
public List<TransactionInformationMatch> Inputs
|
||||
public List<MatchedInput> Inputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<TransactionInformationMatch>();
|
||||
} = new List<MatchedInput>();
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Money BalanceChange
|
||||
public IMoney BalanceChange
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public uint256 ReplacedBy { get; set; }
|
||||
public uint256 Replacing { get; set; }
|
||||
public bool Replaceable { get; set; }
|
||||
public TransactionMetadata Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
22
NBXplorer.Client/Models/GroupInformation.cs
Normal file
22
NBXplorer.Client/Models/GroupInformation.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GroupChild
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string CryptoCode { get; set; }
|
||||
public string TrackedSource { get; set; }
|
||||
}
|
||||
public class GroupInformation
|
||||
{
|
||||
public string TrackedSource { get; set; }
|
||||
public string GroupId { get; set; }
|
||||
public GroupChild[] Children { get; set; }
|
||||
|
||||
public GroupChild AsGroupChild() => new () { TrackedSource = TrackedSource };
|
||||
}
|
||||
}
|
||||
10
NBXplorer.Client/Models/ImportUTXORequest.cs
Normal file
10
NBXplorer.Client/Models/ImportUTXORequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models;
|
||||
|
||||
public class ImportUTXORequest
|
||||
{
|
||||
[JsonProperty("UTXOs")]
|
||||
public OutPoint[] Utxos { get; set; }
|
||||
}
|
||||
@ -1,23 +1,25 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class KeyPathInformation
|
||||
{
|
||||
public TrackedSource TrackedSource { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature Feature
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public DerivationStrategyBase DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPath KeyPath
|
||||
{
|
||||
get; set;
|
||||
@ -26,9 +28,17 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public BitcoinAddress Address { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public Script Redeem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Index { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
|
||||
132
NBXplorer.Client/Models/KeyPathTemplate.cs
Normal file
132
NBXplorer.Client/Models/KeyPathTemplate.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class KeyPathTemplate
|
||||
{
|
||||
private readonly KeyPath preIndexes;
|
||||
private readonly KeyPath postIndexes;
|
||||
|
||||
public KeyPath PostIndexes => postIndexes;
|
||||
|
||||
public KeyPath PreIndexes => preIndexes;
|
||||
|
||||
public static KeyPathTemplate Parse(string path)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
if (!TryParse(path, out var v))
|
||||
throw new FormatException("Invalid keypath template");
|
||||
return v;
|
||||
}
|
||||
public static bool TryParse(string path, out KeyPathTemplate keyPathTemplate)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
bool isValid = true;
|
||||
uint[] preIndices = null, postIndices = null;
|
||||
bool isPreIndex = true;
|
||||
int count = 0;
|
||||
List<uint> indices = new List<uint>();
|
||||
foreach (var p in path
|
||||
.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(p => p != "m"))
|
||||
{
|
||||
if (p == "*")
|
||||
{
|
||||
if (isPreIndex)
|
||||
{
|
||||
isPreIndex = false;
|
||||
count++;
|
||||
preIndices = indices.ToArray();
|
||||
indices.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isValid &= TryParseCore(p, out var i);
|
||||
if(isValid)
|
||||
indices.Add(i);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 255)
|
||||
isValid = false;
|
||||
isValid &= preIndices != null;
|
||||
if (!isValid)
|
||||
{
|
||||
keyPathTemplate = null;
|
||||
return false;
|
||||
}
|
||||
postIndices = indices.ToArray();
|
||||
keyPathTemplate = new KeyPathTemplate(preIndices, postIndices);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseCore(string i, out uint index)
|
||||
{
|
||||
if (i.Length == 0)
|
||||
{
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
bool hardened = i[i.Length - 1] == '\'' || i[i.Length - 1] == 'h';
|
||||
var nonhardened = hardened ? i.Substring(0, i.Length - 1) : i;
|
||||
if (!uint.TryParse(nonhardened, out index))
|
||||
return false;
|
||||
if (hardened)
|
||||
{
|
||||
if (index >= 0x80000000u)
|
||||
{
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
index = index | 0x80000000u;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPathTemplate(uint[] preIndexes, uint[] postIndexes) : this(new KeyPath(preIndexes ?? Array.Empty<uint>()),
|
||||
new KeyPath(postIndexes ?? Array.Empty<uint>()))
|
||||
{
|
||||
}
|
||||
public KeyPathTemplate(KeyPath preIndexes, KeyPath postIndexes)
|
||||
{
|
||||
this.preIndexes = preIndexes ?? new KeyPath();
|
||||
this.postIndexes = postIndexes ?? new KeyPath();
|
||||
}
|
||||
|
||||
public KeyPath GetKeyPath(uint index)
|
||||
{
|
||||
return PreIndexes.Derive(index).Derive(PostIndexes);
|
||||
}
|
||||
public KeyPath GetKeyPath(int index, bool hardened)
|
||||
{
|
||||
return PreIndexes.Derive(index, hardened).Derive(PostIndexes);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(PreIndexes.Length + PostIndexes.Length + 20);
|
||||
if (PreIndexes.Length != 0)
|
||||
builder.Append($"{PreIndexes}/*");
|
||||
else
|
||||
builder.Append("*");
|
||||
if (PostIndexes.Length != 0)
|
||||
builder.Append($"/{PostIndexes}");
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
NBXplorer.Client/Models/KeyPathTemplates.cs
Normal file
48
NBXplorer.Client/Models/KeyPathTemplates.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class KeyPathTemplates
|
||||
{
|
||||
private static readonly KeyPathTemplate depositKeyPathTemplate = KeyPathTemplate.Parse("0/*");
|
||||
private static readonly KeyPathTemplate changeKeyPathTemplate = KeyPathTemplate.Parse("1/*");
|
||||
private static readonly KeyPathTemplate directKeyPathTemplate = KeyPathTemplate.Parse("*");
|
||||
private readonly KeyPathTemplate customKeyPathTemplate;
|
||||
private static readonly KeyPathTemplates _Default = new KeyPathTemplates();
|
||||
private readonly DerivationFeature[] derivationFeatures;
|
||||
public static KeyPathTemplates Default => _Default;
|
||||
|
||||
private KeyPathTemplates() : this(null)
|
||||
{
|
||||
|
||||
}
|
||||
public KeyPathTemplates(KeyPathTemplate customKeyPathTemplate)
|
||||
{
|
||||
this.customKeyPathTemplate = customKeyPathTemplate;
|
||||
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>
|
||||
{
|
||||
DerivationFeature.Deposit,
|
||||
DerivationFeature.Change,
|
||||
DerivationFeature.Direct
|
||||
};
|
||||
if (customKeyPathTemplate != null)
|
||||
derivationFeatures.Add(DerivationFeature.Custom);
|
||||
this.derivationFeatures = derivationFeatures.ToArray();
|
||||
}
|
||||
|
||||
public KeyPathTemplate GetKeyPathTemplate(DerivationFeature derivationFeature)
|
||||
=> derivationFeature switch
|
||||
{
|
||||
DerivationFeature.Deposit => depositKeyPathTemplate,
|
||||
DerivationFeature.Change => changeKeyPathTemplate,
|
||||
DerivationFeature.Direct => directKeyPathTemplate,
|
||||
DerivationFeature.Custom when customKeyPathTemplate != null => customKeyPathTemplate,
|
||||
_ => throw new NotSupportedException($"The derivation feature {derivationFeature} is not supported by the key path templates.")
|
||||
};
|
||||
|
||||
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures() => derivationFeatures;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
@ -28,6 +26,13 @@ namespace NBXplorer.Models
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
public NBXplorerError(int httpCode, string code, string message, string reason)
|
||||
{
|
||||
HttpCode = httpCode;
|
||||
Code = code;
|
||||
Message = message;
|
||||
Reason = reason;
|
||||
}
|
||||
public int HttpCode
|
||||
{
|
||||
get; set;
|
||||
@ -40,6 +45,10 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Reason
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public NBXplorerException AsException()
|
||||
{
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class NewBlockEvent
|
||||
{
|
||||
public class NewBlockEvent : NewEventBase
|
||||
{
|
||||
public NewBlockEvent()
|
||||
{
|
||||
}
|
||||
public int Height
|
||||
{
|
||||
get; set;
|
||||
@ -20,10 +21,15 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CryptoCode
|
||||
|
||||
public long Confirmations { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override string EventType => "newblock";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
get;
|
||||
set;
|
||||
return $"{CryptoCode}: New block {Hash} ({Height})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,7 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class NewBlockEventRequest
|
||||
{
|
||||
public NewBlockEventRequest()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public String CryptoCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public class NewBlockEventRequest : NewEventBase
|
||||
{
|
||||
public override string EventType => "subscribeblock";
|
||||
}
|
||||
}
|
||||
|
||||
108
NBXplorer.Client/Models/NewEventBase.cs
Normal file
108
NBXplorer.Client/Models/NewEventBase.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public abstract class NewEventBase
|
||||
{
|
||||
static NewEventBase()
|
||||
{
|
||||
_TypeByName = new Dictionary<string, Type>();
|
||||
_NameByType = new Dictionary<Type, string>();
|
||||
Add("newblock", typeof(Models.NewBlockEvent));
|
||||
Add("subscribeblock", typeof(Models.NewBlockEventRequest));
|
||||
Add("subscribetransaction", typeof(Models.NewTransactionEventRequest));
|
||||
Add("newtransaction", typeof(Models.NewTransactionEvent));
|
||||
}
|
||||
|
||||
static Dictionary<string, Type> _TypeByName;
|
||||
static Dictionary<Type, string> _NameByType;
|
||||
private static void Add(string typeName, Type type)
|
||||
{
|
||||
_TypeByName.Add(typeName, type);
|
||||
_NameByType.Add(type, typeName);
|
||||
}
|
||||
public static string GetEventTypeName(Type type)
|
||||
{
|
||||
_NameByType.TryGetValue(type, out string name);
|
||||
return name;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public abstract string EventType { get; }
|
||||
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public long EventId { get; set; }
|
||||
|
||||
public JObject ToJObject(JsonSerializerSettings settings)
|
||||
{
|
||||
var typeName = GetEventTypeName(this.GetType());
|
||||
if (typeName == null)
|
||||
throw new InvalidOperationException($"{this.GetType().Name} does not have an associated typeName");
|
||||
JObject jobj = new JObject();
|
||||
var serialized = JsonConvert.SerializeObject(this, settings);
|
||||
var data = JObject.Parse(serialized);
|
||||
if(this.EventId != 0)
|
||||
jobj.Add(new JProperty("eventId", new JValue(EventId)));
|
||||
jobj.Add(new JProperty("type", new JValue(typeName)));
|
||||
jobj.Add(new JProperty("data", data));
|
||||
return jobj;
|
||||
}
|
||||
|
||||
public static NewEventBase ParseEvent(string str, JsonSerializerSettings settings)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
JObject jobj = JObject.Parse(str);
|
||||
return ParseEvent(jobj, settings);
|
||||
}
|
||||
public static NewEventBase ParseEvent(JObject jobj, JsonSerializerSettings settings)
|
||||
{
|
||||
if (jobj == null)
|
||||
throw new ArgumentNullException(nameof(jobj));
|
||||
var type = (jobj["type"] as JValue)?.Value<string>();
|
||||
if (type == null)
|
||||
throw new FormatException("'type' property not found");
|
||||
bool unknown = false;
|
||||
if (!_TypeByName.TryGetValue(type, out Type typeObject))
|
||||
{
|
||||
unknown = true;
|
||||
typeObject = typeof(UnknownEvent);
|
||||
}
|
||||
var data = (jobj["data"] as JObject);
|
||||
if (data == null)
|
||||
throw new FormatException("'data' property not found");
|
||||
|
||||
NewEventBase evt = null;
|
||||
if (unknown)
|
||||
{
|
||||
var unk = new UnknownEvent(type);
|
||||
unk.Data = data;
|
||||
unk.CryptoCode = data["cryptoCode"]?.Value<string>();
|
||||
evt = unk;
|
||||
}
|
||||
else
|
||||
{
|
||||
evt = (NewEventBase)JsonConvert.DeserializeObject(data.ToString(), typeObject, settings);
|
||||
}
|
||||
if(jobj["eventId"] != null)
|
||||
{
|
||||
evt.EventId = jobj["eventId"].Value<long>();
|
||||
}
|
||||
return evt;
|
||||
}
|
||||
|
||||
public string ToJson(JsonSerializerSettings settings)
|
||||
{
|
||||
return JsonConvert.SerializeObject(this, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,23 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class NewTransactionEvent
|
||||
public class NewTransactionEvent : NewEventBase
|
||||
{
|
||||
public uint256 BlockId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public TrackedSource TrackedSource { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
|
||||
public DerivationStrategyBase DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
@ -23,24 +28,57 @@ namespace NBXplorer.Models
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<KeyPathInformation> Outputs
|
||||
public List<MatchedInput> Inputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<KeyPathInformation>();
|
||||
|
||||
public List<KeyPathInformation> Inputs
|
||||
} = new List<MatchedInput>();
|
||||
public List<MatchedOutput> Outputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<KeyPathInformation>();
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
} = new List<MatchedOutput>();
|
||||
|
||||
public TransactionMatch AsMatch()
|
||||
[JsonIgnore]
|
||||
public override string EventType => "newtransaction";
|
||||
|
||||
public List<uint256> Replacing { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return new TransactionMatch() { DerivationStrategy = DerivationStrategy, Inputs = Inputs, Outputs = Outputs, Transaction = TransactionData.Transaction };
|
||||
var conf = (BlockId == null ? "unconfirmed" : "confirmed");
|
||||
|
||||
string strategy = TrackedSource.ToPrettyString();
|
||||
var txId = TransactionData.TransactionHash.ToString();
|
||||
txId = txId.Substring(0, 6) + "..." + txId.Substring(txId.Length - 6);
|
||||
|
||||
string keyPathSuffix = string.Empty;
|
||||
var keyPaths = Outputs.Select(v => v.KeyPath?.ToString()).Where(k => k != null).ToArray();
|
||||
if (keyPaths.Length != 0)
|
||||
{
|
||||
keyPathSuffix = $" ({String.Join(", ", keyPaths)})";
|
||||
}
|
||||
return $"{CryptoCode}: {strategy} matching {conf} transaction {txId}{keyPathSuffix}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MatchedOutput
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPath KeyPath { get; set; }
|
||||
public Script ScriptPubKey { get; set; }
|
||||
public int Index { get; set; }
|
||||
public int KeyIndex { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature? Feature { get; set; }
|
||||
public IMoney Value { get; set; }
|
||||
public BitcoinAddress Address { get; set; }
|
||||
}
|
||||
|
||||
public class MatchedInput : MatchedOutput
|
||||
{
|
||||
public int InputIndex { get; set; }
|
||||
public uint256 TransactionId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,12 @@
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class NewTransactionEventRequest
|
||||
{
|
||||
public NewTransactionEventRequest()
|
||||
{
|
||||
public class NewTransactionEventRequest : NewEventBase
|
||||
{
|
||||
public override string EventType => "subscribetransaction";
|
||||
public string[] DerivationSchemes { get; set; }
|
||||
|
||||
}
|
||||
public string CryptoCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string[] DerivationSchemes
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string[] TrackedSources { get; set; }
|
||||
public bool? ListenAllTrackedSource { get; set; }
|
||||
public bool? ListenAllDerivationSchemes { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
7
NBXplorer.Client/Models/PruneRequest.cs
Normal file
7
NBXplorer.Client/Models/PruneRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class PruneRequest
|
||||
{
|
||||
public double? DaysToKeep { get; set; }
|
||||
}
|
||||
}
|
||||
7
NBXplorer.Client/Models/PruneResponse.cs
Normal file
7
NBXplorer.Client/Models/PruneResponse.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class PruneResponse
|
||||
{
|
||||
public int TotalPruned { get; set; }
|
||||
}
|
||||
}
|
||||
28
NBXplorer.Client/Models/RescanRequest.cs
Normal file
28
NBXplorer.Client/Models/RescanRequest.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class RescanRequest
|
||||
{
|
||||
public class TransactionToRescan
|
||||
{
|
||||
public uint256 BlockId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public uint256 TransactionId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Transaction Transaction
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public List<TransactionToRescan> Transactions
|
||||
{
|
||||
get; set;
|
||||
} = new List<TransactionToRescan>();
|
||||
}
|
||||
}
|
||||
96
NBXplorer.Client/Models/ScanUTXOInformation.cs
Normal file
96
NBXplorer.Client/Models/ScanUTXOInformation.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public enum ScanUTXOStatus
|
||||
{
|
||||
Queued,
|
||||
Pending,
|
||||
Error,
|
||||
Complete
|
||||
}
|
||||
|
||||
public class ScanUTXOProgress
|
||||
{
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.UnixDateTimeConverter))]
|
||||
public DateTimeOffset StartedAt { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.UnixDateTimeConverter))]
|
||||
public DateTimeOffset? CompletedAt { get; set; }
|
||||
public int Found { get; set; }
|
||||
public int BatchNumber { get; set; }
|
||||
public int RemainingBatches { get; set; }
|
||||
public int CurrentBatchProgress { get; set; }
|
||||
public int OverallProgress { get; set; }
|
||||
public int RemainingSeconds { get; set; }
|
||||
public int From { get; set; }
|
||||
public int Count { get; set; }
|
||||
public int TotalSearched { get; set; }
|
||||
public int? TotalSizeOfUTXOSet { get; set; }
|
||||
|
||||
public void UpdateOverallProgress(DateTimeOffset? now = null)
|
||||
{
|
||||
now = now.HasValue ? now : DateTimeOffset.UtcNow;
|
||||
double percentPerBatch = 100.0 / (RemainingBatches + BatchNumber + 1);
|
||||
double batchPercent = BatchNumber * percentPerBatch;
|
||||
double insideBatchPercent = CurrentBatchProgress * (percentPerBatch / 100.0);
|
||||
var overallProgress = batchPercent + insideBatchPercent;
|
||||
|
||||
var timeSpent = now.Value - StartedAt;
|
||||
var secondsRemaining = ((100 - overallProgress) / 100.0) * timeSpent.TotalSeconds;
|
||||
OverallProgress = (int)Math.Round(overallProgress);
|
||||
RemainingSeconds = (int)Math.Round(secondsRemaining);
|
||||
}
|
||||
|
||||
public void UpdateRemainingBatches(int gapLimit)
|
||||
{
|
||||
int highestIndex = -1;
|
||||
foreach(var index in HighestKeyIndexFound)
|
||||
{
|
||||
if(index.Value != null)
|
||||
{
|
||||
if (index.Value > highestIndex)
|
||||
highestIndex = index.Value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
int gapLimitIndex = highestIndex + gapLimit;
|
||||
var totalBatchesRequired = (gapLimitIndex / Count) + 1;
|
||||
RemainingBatches = totalBatchesRequired - (BatchNumber + 1);
|
||||
}
|
||||
|
||||
|
||||
public Dictionary<DerivationFeature, int?> HighestKeyIndexFound { get; set; } = new Dictionary<DerivationFeature, int?>();
|
||||
|
||||
public ScanUTXOProgress Clone()
|
||||
{
|
||||
return new ScanUTXOProgress()
|
||||
{
|
||||
Found = Found,
|
||||
BatchNumber = BatchNumber,
|
||||
CurrentBatchProgress = CurrentBatchProgress,
|
||||
From = From,
|
||||
Count = Count,
|
||||
HighestKeyIndexFound = new Dictionary<DerivationFeature, int?>(HighestKeyIndexFound),
|
||||
TotalSearched = TotalSearched,
|
||||
OverallProgress = OverallProgress,
|
||||
RemainingBatches = RemainingBatches,
|
||||
CompletedAt = CompletedAt,
|
||||
StartedAt = StartedAt,
|
||||
RemainingSeconds = RemainingSeconds
|
||||
};
|
||||
}
|
||||
}
|
||||
public class ScanUTXOInformation
|
||||
{
|
||||
public string Error { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.UnixDateTimeConverter))]
|
||||
public DateTimeOffset QueuedAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public ScanUTXOStatus Status { get; set; }
|
||||
public ScanUTXOProgress Progress { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
@ -35,18 +32,22 @@ namespace NBXplorer.Models
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string[] ExternalAddresses { get; set; }
|
||||
public NodeCapabilities Capabilities { get; set; }
|
||||
}
|
||||
public class StatusResult
|
||||
public class NodeCapabilities
|
||||
{
|
||||
public bool CanScanTxoutSet { get; set; }
|
||||
public bool CanSupportSegwit { get; set; }
|
||||
public bool CanSupportTaproot { get; set; }
|
||||
public bool CanSupportTransactionCheck { get; set; }
|
||||
}
|
||||
public class StatusResult
|
||||
{
|
||||
public BitcoinStatus BitcoinStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public double RepositoryPingTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool IsFullySynched
|
||||
{
|
||||
get; set;
|
||||
@ -61,8 +62,13 @@ namespace NBXplorer.Models
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public NetworkType NetworkType
|
||||
public string InstanceName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.ChainNameJsonConverter))]
|
||||
public ChainName NetworkType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
19
NBXplorer.Client/Models/TrackWalletRequest.cs
Normal file
19
NBXplorer.Client/Models/TrackWalletRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class TrackWalletRequest
|
||||
{
|
||||
public TrackDerivationOption[] DerivationOptions { get; set; }
|
||||
public bool Wait { get; set; } = false;
|
||||
}
|
||||
|
||||
public class TrackDerivationOption
|
||||
{
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature? Feature { get; set; }
|
||||
public int? MinAddresses { get; set; }
|
||||
public int? MaxAddresses { get; set; }
|
||||
}
|
||||
}
|
||||
251
NBXplorer.Client/Models/TrackedSource.cs
Normal file
251
NBXplorer.Client/Models/TrackedSource.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public abstract class TrackedSource
|
||||
{
|
||||
public static bool TryParse(string str, out TrackedSource trackedSource, NBXplorerNetwork network)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
trackedSource = null;
|
||||
var strSpan = str.AsSpan();
|
||||
if (strSpan.StartsWith("DERIVATIONSCHEME:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (network is null)
|
||||
return false;
|
||||
if (!DerivationSchemeTrackedSource.TryParse(strSpan, out var derivationSchemeTrackedSource, network))
|
||||
return false;
|
||||
trackedSource = derivationSchemeTrackedSource;
|
||||
}
|
||||
else if (strSpan.StartsWith("ADDRESS:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (network is null)
|
||||
return false;
|
||||
if (!AddressTrackedSource.TryParse(strSpan, out var addressTrackedSource, network.NBitcoinNetwork))
|
||||
return false;
|
||||
trackedSource = addressTrackedSource;
|
||||
}
|
||||
else if (strSpan.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (!GroupTrackedSource.TryParse(strSpan, out var walletTrackedSource))
|
||||
return false;
|
||||
trackedSource = walletTrackedSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
TrackedSource item = obj as TrackedSource;
|
||||
if (item == null)
|
||||
return false;
|
||||
return ToString().Equals(item.ToString());
|
||||
}
|
||||
public static bool operator ==(TrackedSource a, TrackedSource b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.ToString() == b.ToString();
|
||||
}
|
||||
|
||||
public static bool operator !=(TrackedSource a, TrackedSource b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ToString().GetHashCode();
|
||||
}
|
||||
|
||||
public static DerivationSchemeTrackedSource Create(DerivationStrategyBase strategy)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
return new DerivationSchemeTrackedSource(strategy);
|
||||
}
|
||||
public static AddressTrackedSource Create(BitcoinAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
return new AddressTrackedSource(address);
|
||||
}
|
||||
public static AddressTrackedSource Create(Script scriptPubKey, Network network)
|
||||
{
|
||||
if (scriptPubKey == null)
|
||||
throw new ArgumentNullException(nameof(scriptPubKey));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
|
||||
var address = scriptPubKey.GetDestinationAddress(network);
|
||||
if (address == null)
|
||||
throw new ArgumentException(paramName: nameof(scriptPubKey), message: $"{nameof(scriptPubKey)} can't be translated on an address on {network.Name}");
|
||||
return new AddressTrackedSource(address);
|
||||
}
|
||||
|
||||
public virtual string ToPrettyString()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
public static TrackedSource Parse(string str, NBXplorerNetwork network)
|
||||
{
|
||||
if (!TryParse(str, out var trackedSource, network))
|
||||
throw new FormatException("Invalid TrackedSource");
|
||||
return trackedSource;
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupTrackedSource : TrackedSource
|
||||
{
|
||||
public string GroupId { get; }
|
||||
|
||||
public static GroupTrackedSource Generate()
|
||||
{
|
||||
Span<byte> r = stackalloc byte[13];
|
||||
// 13 is most consistent on number of chars and more than we need to avoid generating twice same id
|
||||
RandomNumberGenerator.Fill(r);
|
||||
return new GroupTrackedSource(Encoders.Base58.EncodeData(r));
|
||||
}
|
||||
|
||||
public GroupTrackedSource(string groupId)
|
||||
{
|
||||
GroupId = groupId;
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> trackedSource, out GroupTrackedSource walletTrackedSource)
|
||||
{
|
||||
walletTrackedSource = null;
|
||||
if (!trackedSource.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
walletTrackedSource = new GroupTrackedSource(trackedSource.Slice("GROUP:".Length).ToString());
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "GROUP:" + GroupId;
|
||||
}
|
||||
public override string ToPrettyString()
|
||||
{
|
||||
return "G:" + GroupId;
|
||||
}
|
||||
|
||||
public static GroupTrackedSource Parse(string trackedSource)
|
||||
{
|
||||
return TryParse(trackedSource, out var g) ? g : throw new FormatException("Invalid group tracked source format");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressTrackedSource : TrackedSource, IDestination
|
||||
{
|
||||
// Note that we should in theory access BitcoinAddress. But parsing BitcoinAddress is very expensive, so we keep storing plain strings
|
||||
public AddressTrackedSource(BitcoinAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
_FullAddressString = "ADDRESS:" + address;
|
||||
Address = address;
|
||||
}
|
||||
|
||||
string _FullAddressString;
|
||||
|
||||
public BitcoinAddress Address
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Script ScriptPubKey => Address.ScriptPubKey;
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> strSpan, out TrackedSource addressTrackedSource, Network network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
addressTrackedSource = null;
|
||||
if (!strSpan.StartsWith("ADDRESS:".AsSpan(), StringComparison.Ordinal))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
addressTrackedSource = new AddressTrackedSource(BitcoinAddress.Create(strSpan.Slice("ADDRESS:".Length).ToString(), network));
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _FullAddressString;
|
||||
}
|
||||
|
||||
public override string ToPrettyString()
|
||||
{
|
||||
return Address.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class DerivationSchemeTrackedSource : TrackedSource
|
||||
{
|
||||
public DerivationSchemeTrackedSource(DerivationStrategy.DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
DerivationStrategy = derivationStrategy;
|
||||
}
|
||||
|
||||
public DerivationStrategy.DerivationStrategyBase DerivationStrategy { get; }
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> strSpan, out DerivationSchemeTrackedSource derivationSchemeTrackedSource, NBXplorerNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
derivationSchemeTrackedSource = null;
|
||||
if (!strSpan.StartsWith("DERIVATIONSCHEME:".AsSpan(), StringComparison.Ordinal))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var factory = network.DerivationStrategyFactory;
|
||||
var derivationScheme = factory.Parse(strSpan.Slice("DERIVATIONSCHEME:".Length).ToString());
|
||||
derivationSchemeTrackedSource = new DerivationSchemeTrackedSource(derivationScheme);
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "DERIVATIONSCHEME:" + DerivationStrategy.ToString();
|
||||
}
|
||||
public override string ToPrettyString()
|
||||
{
|
||||
var strategy = DerivationStrategy.ToString();
|
||||
if (strategy.Length > 35)
|
||||
{
|
||||
strategy = strategy.Substring(0, 10) + "..." + strategy.Substring(strategy.Length - 20);
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
#if !NO_RECORD
|
||||
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
|
||||
=> DerivationStrategy is PolicyDerivationStrategy ? new[] { DerivationFeature.Deposit, DerivationFeature.Change } : keyPathTemplates.GetSupportedDerivationFeatures();
|
||||
#else
|
||||
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
|
||||
=> keyPathTemplates.GetSupportedDerivationFeatures();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class TransactionMatch
|
||||
{
|
||||
public DerivationStrategyBase DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Transaction Transaction
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<KeyPathInformation> Outputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<KeyPathInformation>();
|
||||
|
||||
public List<KeyPathInformation> Inputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<KeyPathInformation>();
|
||||
}
|
||||
}
|
||||
40
NBXplorer.Client/Models/TransactionMetadata.cs
Normal file
40
NBXplorer.Client/Models/TransactionMetadata.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class TransactionMetadata
|
||||
{
|
||||
public class ChunkMetadata
|
||||
{
|
||||
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
|
||||
public Money Fees { get; set; }
|
||||
[JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Weight { get; set; }
|
||||
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("vsize", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? VirtualSize { get; set; }
|
||||
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
|
||||
public Money Fees { get; set; }
|
||||
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
public ChunkMetadata Chunk { get; set; }
|
||||
public static TransactionMetadata Parse(string json) => JsonConvert.DeserializeObject<TransactionMetadata>(json);
|
||||
public string ToString(bool indented) => JsonConvert.SerializeObject(this, indented ? Formatting.Indented : Formatting.None);
|
||||
public override string ToString() => ToString(true);
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,20 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class TransactionResult
|
||||
{
|
||||
uint _Confirmations;
|
||||
public int Confirmations
|
||||
long _Confirmations;
|
||||
public long Confirmations
|
||||
{
|
||||
get
|
||||
{
|
||||
return checked((int)_Confirmations);
|
||||
return _Confirmations;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Confirmations = checked((uint)value);
|
||||
_Confirmations = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +30,7 @@ namespace NBXplorer.Models
|
||||
_BlockId = value;
|
||||
}
|
||||
}
|
||||
|
||||
public uint256 TransactionHash { get; set; }
|
||||
Transaction _Transaction;
|
||||
public Transaction Transaction
|
||||
{
|
||||
@ -46,7 +44,7 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public int? Height
|
||||
public long? Height
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@ -56,5 +54,8 @@ namespace NBXplorer.Models
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public uint256 ReplacedBy { get; set; }
|
||||
|
||||
public TransactionMetadata Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
using NBitcoin;
|
||||
using System.Linq;
|
||||
using NBitcoin.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin.Crypto;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class UTXOChanges
|
||||
{
|
||||
public TrackedSource TrackedSource { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public DerivationStrategyBase DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
@ -44,6 +45,11 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public List<UTXO> SpentUnconfirmed
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<UTXO>();
|
||||
|
||||
UTXOChange _Confirmed = new UTXOChange();
|
||||
public UTXOChange Confirmed
|
||||
@ -58,69 +64,41 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasChanges
|
||||
{
|
||||
get
|
||||
{
|
||||
return Confirmed.HasChanges || Unconfirmed.HasChanges;
|
||||
}
|
||||
}
|
||||
|
||||
public Coin[] GetUnspentCoins(bool excludeUnconfirmedUTXOs = false)
|
||||
{
|
||||
if(Confirmed.KnownBookmark != null || Unconfirmed.KnownBookmark != null)
|
||||
throw new InvalidOperationException("This UTXOChanges is partial, it is calculate the unspent coins");
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Select(c => c.AsCoin(DerivationStrategy)).ToArray();
|
||||
return GetUnspentCoins(excludeUnconfirmedUTXOs ? 1 : 0);
|
||||
}
|
||||
|
||||
public UTXO[] GetUnspentUTXOs(bool excludeUnconfirmedUTXOs = false)
|
||||
public Coin[] GetUnspentCoins(int minConfirmations)
|
||||
{
|
||||
return GetUnspentUTXOs(minConfirmations).Select(c => c.AsCoin(DerivationStrategy)).ToArray();
|
||||
}
|
||||
public UTXO[] GetUnspentUTXOs(int minConf)
|
||||
{
|
||||
var excludeUnconfirmedUTXOs = minConf > 0;
|
||||
Dictionary<OutPoint, UTXO> received = new Dictionary<OutPoint, UTXO>();
|
||||
foreach(var utxo in Confirmed.UTXOs.Concat(excludeUnconfirmedUTXOs ? (IEnumerable<UTXO>)Array.Empty<UTXO>() : Unconfirmed.UTXOs))
|
||||
foreach (var utxo in Confirmed.UTXOs.Where(u => u.Confirmations >= minConf).Concat(excludeUnconfirmedUTXOs ? (IEnumerable<UTXO>)Array.Empty<UTXO>() : Unconfirmed.UTXOs))
|
||||
{
|
||||
received.TryAdd(utxo.Outpoint, utxo);
|
||||
}
|
||||
foreach(var utxo in Confirmed.SpentOutpoints.Concat(Unconfirmed.SpentOutpoints))
|
||||
foreach (var utxo in Confirmed.SpentOutpoints.Concat(Unconfirmed.SpentOutpoints))
|
||||
{
|
||||
received.Remove(utxo);
|
||||
}
|
||||
return received.Values.ToArray();
|
||||
}
|
||||
public UTXO[] GetUnspentUTXOs(bool excludeUnconfirmedUTXOs = false)
|
||||
{
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs ? 1 : 0);
|
||||
}
|
||||
|
||||
public Key[] GetKeys(ExtKey extKey, bool excludeUnconfirmedUTXOs = false)
|
||||
{
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Where(u => u.KeyPath is not null).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
|
||||
}
|
||||
}
|
||||
public class UTXOChange
|
||||
{
|
||||
|
||||
Bookmark _KnownBookmark;
|
||||
public Bookmark KnownBookmark
|
||||
{
|
||||
get
|
||||
{
|
||||
return _KnownBookmark;
|
||||
}
|
||||
set
|
||||
{
|
||||
_KnownBookmark = value;
|
||||
}
|
||||
}
|
||||
|
||||
Bookmark _Bookmark = null;
|
||||
public Bookmark Bookmark
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Bookmark;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Bookmark = value;
|
||||
}
|
||||
}
|
||||
|
||||
List<UTXO> _UTXOs = new List<UTXO>();
|
||||
public List<UTXO> UTXOs
|
||||
{
|
||||
@ -146,14 +124,6 @@ namespace NBXplorer.Models
|
||||
_SpentOutpoints = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasChanges
|
||||
{
|
||||
get
|
||||
{
|
||||
return KnownBookmark != Bookmark || UTXOs.Count != 0 || SpentOutpoints.Count != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UTXO
|
||||
@ -163,15 +133,18 @@ namespace NBXplorer.Models
|
||||
|
||||
}
|
||||
|
||||
public UTXO(Coin coin)
|
||||
public UTXO(ICoin coin)
|
||||
{
|
||||
Outpoint = coin.Outpoint;
|
||||
Value = coin.TxOut.Value;
|
||||
Index = (int)coin.Outpoint.N;
|
||||
TransactionHash = coin.Outpoint.Hash;
|
||||
Value = coin.Amount;
|
||||
ScriptPubKey = coin.TxOut.ScriptPubKey;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature Feature
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public DerivationFeature? Feature
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -183,16 +156,37 @@ namespace NBXplorer.Models
|
||||
|
||||
public Coin AsCoin(DerivationStrategy.DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
var coin = new Coin(Outpoint, new TxOut(Value, ScriptPubKey));
|
||||
if(derivationStrategy != null)
|
||||
if (Value is Money v)
|
||||
{
|
||||
var derivation = derivationStrategy.Derive(KeyPath);
|
||||
if(derivation.ScriptPubKey != coin.ScriptPubKey)
|
||||
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
|
||||
if(derivation.Redeem != null)
|
||||
coin = coin.ToScriptCoin(derivation.Redeem);
|
||||
var coin = new Coin(Outpoint, new TxOut(v, ScriptPubKey));
|
||||
if (Redeem is not null)
|
||||
{
|
||||
coin = coin.ToScriptCoin(Redeem);
|
||||
}
|
||||
else
|
||||
{
|
||||
DerivationStrategy.Derivation derivation = null;
|
||||
if (derivationStrategy is StandardDerivationStrategyBase kd && KeyPath is not null)
|
||||
{
|
||||
derivation = kd.GetDerivation(KeyPath);
|
||||
}
|
||||
#if !NO_RECORD
|
||||
else if (derivationStrategy is PolicyDerivationStrategy md && Feature is { } f)
|
||||
{
|
||||
derivation = md.GetDerivation(f, (uint)KeyIndex);
|
||||
}
|
||||
#endif
|
||||
if (derivation is not null)
|
||||
{
|
||||
if (derivation.ScriptPubKey != coin.ScriptPubKey)
|
||||
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
|
||||
if (derivation.Redeem != null)
|
||||
coin = coin.ToScriptCoin(derivation.Redeem);
|
||||
}
|
||||
}
|
||||
return coin;
|
||||
}
|
||||
return coin;
|
||||
return null;
|
||||
}
|
||||
|
||||
OutPoint _Outpoint = new OutPoint();
|
||||
@ -208,7 +202,8 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int Index { get; set; }
|
||||
public uint256 TransactionHash { get; set; }
|
||||
|
||||
Script _ScriptPubKey;
|
||||
public Script ScriptPubKey
|
||||
@ -223,9 +218,12 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public BitcoinAddress Address { get; set; }
|
||||
|
||||
Money _Value;
|
||||
public Money Value
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public Script Redeem { get; set; }
|
||||
IMoney _Value;
|
||||
public IMoney Value
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -239,6 +237,7 @@ namespace NBXplorer.Models
|
||||
|
||||
KeyPath _KeyPath;
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPath KeyPath
|
||||
{
|
||||
get
|
||||
@ -266,17 +265,19 @@ namespace NBXplorer.Models
|
||||
}
|
||||
|
||||
|
||||
uint _Confirmations;
|
||||
public int Confirmations
|
||||
long _Confirmations;
|
||||
public long Confirmations
|
||||
{
|
||||
get
|
||||
{
|
||||
return checked((int)_Confirmations);
|
||||
return checked((long)_Confirmations);
|
||||
}
|
||||
set
|
||||
{
|
||||
_Confirmations = checked((uint)value);
|
||||
_Confirmations = checked((long)value);
|
||||
}
|
||||
}
|
||||
|
||||
public int KeyIndex { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
20
NBXplorer.Client/Models/UnknownEvent.cs
Normal file
20
NBXplorer.Client/Models/UnknownEvent.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class UnknownEvent : NewEventBase
|
||||
{
|
||||
public UnknownEvent()
|
||||
{
|
||||
|
||||
}
|
||||
public UnknownEvent(string eventType)
|
||||
{
|
||||
_EventType = eventType;
|
||||
}
|
||||
string _EventType;
|
||||
public override string EventType => _EventType;
|
||||
|
||||
public JObject Data { get; set; }
|
||||
}
|
||||
}
|
||||
37
NBXplorer.Client/Models/UpdatePSBTRequest.cs
Normal file
37
NBXplorer.Client/Models/UpdatePSBTRequest.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class UpdatePSBTRequest
|
||||
{
|
||||
[JsonProperty("psbt")]
|
||||
public PSBT PSBT { get; set; }
|
||||
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rebase the hdkey paths (if no rebase, the key paths are relative to the xpub that NBXplorer knows about)
|
||||
/// This transform (PubKey0, 0/0, accountFingerprint) by (PubKey0, m/49'/0'/0/0, masterFingerprint)
|
||||
/// </summary>
|
||||
public List<PSBTRebaseKeyRules> RebaseKeyPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempt setting non_witness_utxo for all inputs even if they are segwit.
|
||||
/// </summary>
|
||||
public bool AlwaysIncludeNonWitnessUTXO { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the include the global xpub in the PSBT (default: false)
|
||||
/// </summary>
|
||||
public bool? IncludeGlobalXPub { get; set; }
|
||||
}
|
||||
public class UpdatePSBTResponse
|
||||
{
|
||||
[JsonProperty("psbt")]
|
||||
public PSBT PSBT { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
|
||||
<Company>Digital Garage</Company>
|
||||
<Version>1.0.2.7</Version>
|
||||
<Version>5.0.6</Version>
|
||||
<Copyright>Copyright © Digital Garage 2017</Copyright>
|
||||
<Description>Client API for the minimalist HD Wallet Tracker NBXplorer</Description>
|
||||
<PackageIconUrl>https://aois.blob.core.windows.net/public/Bitcoin.png</PackageIconUrl>
|
||||
<PackageIcon>Bitcoin.png</PackageIcon>
|
||||
<PackageTags>bitcoin</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/dgarage/NBXplorer/</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/dgarage/NBXplorer/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://github.com/dgarage/NBXplorer/</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/btcpayserver/NBXplorer/</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/NBXplorer</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
|
||||
<DefineConstants>$(DefineConstants);NO_RECORD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591;1573;1572;1584;1570;3021</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.6" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="1.0.1.4" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||
<PackageReference Include="NBitcoin" Version="10.0.6" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="6.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="NBXplorer.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="Bitcoin.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -2,44 +2,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class NBXplorerDefaultSettings
|
||||
{
|
||||
static NBXplorerDefaultSettings()
|
||||
|
||||
public static string GetFolderName(ChainName chainName)
|
||||
{
|
||||
_Settings = new Dictionary<NetworkType, NBXplorerDefaultSettings>();
|
||||
foreach(var networkType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
|
||||
{
|
||||
var settings = new NBXplorerDefaultSettings();
|
||||
_Settings.Add(networkType, settings);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", GetFolderName(networkType));
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultCookieFile = Path.Combine(settings.DefaultDataDirectory, ".cookie");
|
||||
settings.DefaultPort = (networkType == NetworkType.Mainnet ? 24444 :
|
||||
networkType == NetworkType.Regtest ? 24446 :
|
||||
networkType == NetworkType.Testnet ? 24445 : throw new NotSupportedException(networkType.ToString()));
|
||||
settings.DefaultUrl = new Uri($"http://127.0.0.1:{settings.DefaultPort}/", UriKind.Absolute);
|
||||
}
|
||||
if (chainName == null)
|
||||
throw new ArgumentNullException(nameof(chainName));
|
||||
if (chainName == ChainName.Mainnet)
|
||||
return "Main";
|
||||
if (chainName == ChainName.Testnet)
|
||||
return "TestNet";
|
||||
if (chainName == ChainName.Regtest)
|
||||
return "RegTest";
|
||||
return chainName.ToString();
|
||||
}
|
||||
|
||||
public static string GetFolderName(NetworkType networkType)
|
||||
{
|
||||
switch(networkType)
|
||||
{
|
||||
case NetworkType.Mainnet:
|
||||
return "Main";
|
||||
case NetworkType.Regtest:
|
||||
return "RegTest";
|
||||
case NetworkType.Testnet:
|
||||
return "TestNet";
|
||||
}
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static Dictionary<NetworkType, NBXplorerDefaultSettings> _Settings;
|
||||
static Dictionary<ChainName, NBXplorerDefaultSettings> _Settings = new Dictionary<ChainName, NBXplorerDefaultSettings>();
|
||||
public string DefaultDataDirectory
|
||||
{
|
||||
get;
|
||||
@ -66,9 +48,68 @@ namespace NBXplorer
|
||||
set;
|
||||
}
|
||||
|
||||
public static NBXplorerDefaultSettings GetDefaultSettings(NetworkType networkType)
|
||||
public static NBXplorerDefaultSettings GetDefaultSettings(ChainName networkType)
|
||||
{
|
||||
return _Settings[networkType];
|
||||
if (_Settings.TryGetValue(networkType, out var v))
|
||||
return v;
|
||||
lock (_Settings)
|
||||
{
|
||||
if (_Settings.TryGetValue(networkType, out v))
|
||||
return v;
|
||||
var settings = new NBXplorerDefaultSettings();
|
||||
settings.DefaultDataDirectory = GetDirectory("NBXplorer", GetFolderName(networkType), false);
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultCookieFile = Path.Combine(settings.DefaultDataDirectory, ".cookie");
|
||||
settings.DefaultPort = (networkType == ChainName.Mainnet ? 24444 :
|
||||
networkType == ChainName.Regtest ? 24446 :
|
||||
networkType == ChainName.Testnet ? 24445 : 24447);
|
||||
settings.DefaultUrl = new Uri($"http://127.0.0.1:{settings.DefaultPort}/", UriKind.Absolute);
|
||||
_Settings.Add(networkType, settings);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
static string GetDirectory(string appDirectory, string subDirectory, bool createIfNotExists = true)
|
||||
{
|
||||
string directory = null;
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
var localAppData = Environment.GetEnvironmentVariable("APPDATA");
|
||||
if (!string.IsNullOrEmpty(home) && string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
directory = home;
|
||||
directory = Path.Combine(directory, "." + appDirectory.ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
directory = localAppData;
|
||||
directory = Path.Combine(directory, appDirectory);
|
||||
}
|
||||
else if (createIfNotExists)
|
||||
{
|
||||
throw new DirectoryNotFoundException("Could not find suitable datadir environment variables HOME or APPDATA are not set");
|
||||
}
|
||||
else
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (createIfNotExists)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
directory = Path.Combine(directory, subDirectory);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
directory = Path.Combine(directory, subDirectory);
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class NBXplorerNetwork
|
||||
{
|
||||
public NBXplorerNetwork(INetworkSet networkSet, NBitcoin.NetworkType networkType)
|
||||
internal NBXplorerNetwork(INetworkSet networkSet, ChainName networkType)
|
||||
{
|
||||
NBitcoinNetwork = networkSet.GetNetwork(networkType);
|
||||
CryptoCode = networkSet.CryptoCode;
|
||||
DefaultSettings = NBXplorerDefaultSettings.GetDefaultSettings(networkType);
|
||||
}
|
||||
public static uint256 UnknownTxId = uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
public static string UnknownAssetId = uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").ToString();
|
||||
public static AssetMoney UnknownAssetMoney = new AssetMoney(uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), 1);
|
||||
public bool IsElement => NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance;
|
||||
public Network NBitcoinNetwork
|
||||
{
|
||||
get;
|
||||
@ -37,31 +39,80 @@ namespace NBXplorer
|
||||
private set;
|
||||
}
|
||||
|
||||
internal virtual DerivationStrategyFactory CreateStrategyFactory()
|
||||
{
|
||||
return new DerivationStrategy.DerivationStrategyFactory(NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public DerivationStrategy.DerivationStrategyFactory DerivationStrategyFactory
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public virtual BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
|
||||
{
|
||||
return scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public bool SupportCookieAuthentication
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
} = true;
|
||||
|
||||
|
||||
private Serializer _Serializer;
|
||||
public Serializer Serializer
|
||||
{
|
||||
get
|
||||
{
|
||||
_Serializer ??= new Serializer(this);
|
||||
return _Serializer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public JsonSerializerSettings JsonSerializerSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
return Serializer.Settings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public TimeSpan ChainLoadingTimeout
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(15);
|
||||
public bool SupportEstimatesSmartFee
|
||||
|
||||
public TimeSpan ChainCacheLoadingTimeout
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
} = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Minimum blocks to keep if pruning is activated
|
||||
/// </summary>
|
||||
public int MinBlocksToKeep
|
||||
{
|
||||
get; set;
|
||||
} = 288;
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode.ToString();
|
||||
}
|
||||
|
||||
public virtual ExplorerClient CreateExplorerClient(Uri uri)
|
||||
{
|
||||
return new ExplorerClient(this, uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Althash.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Althash.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitAlthash(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Althash.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 169900,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("172'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetALTHASH()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Althash.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Argoneum.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Argoneum.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitArgoneum(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Argoneum.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 1040000,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("421'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetAGM()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Argoneum.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,15 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitBCash(NetworkType networkType)
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitBCash(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.BCash.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("145'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,12 @@ namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitBGold(NetworkType networkType)
|
||||
private void InitBGold(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.BGold.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitBitcoin(NetworkType networkType)
|
||||
private void InitBitcoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Bitcoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 150000
|
||||
MinRPCVersion = 150000,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Bitcore.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Bitcore.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitBitcore(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Bitcore.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 80007,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("160'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetBTX()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Bitcore.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Chaincoin.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Chaincoin.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitChaincoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Chaincoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 160400,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("711'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetCHC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Chaincoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
NBXplorer.Client/NBXplorerNetworkProvider.Colossus.cs
Normal file
20
NBXplorer.Client/NBXplorerNetworkProvider.Colossus.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitColossus(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Colossus.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 1010000
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetCOLX()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Colossus.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,15 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitDash(NetworkType networkType)
|
||||
private void InitDash(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Dash.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 120000
|
||||
MinRPCVersion = 120000,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("5'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitDogecoin(NetworkType networkType)
|
||||
private void InitDogecoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Dogecoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200,
|
||||
ChainLoadingTimeout = TimeSpan.FromHours(1),
|
||||
SupportCookieAuthentication = false,
|
||||
SupportEstimatesSmartFee = false
|
||||
ChainCacheLoadingTimeout = TimeSpan.FromMinutes(2),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Feathercoin.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Feathercoin.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitFeathercoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Feathercoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 160000,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetFTC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Feathercoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Gobyte.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Gobyte.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitGobyte(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.GoByte.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 120204,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("176'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetGBX()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.GoByte.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
NBXplorer.Client/NBXplorerNetworkProvider.Groestlcoin.cs
Normal file
16
NBXplorer.Client/NBXplorerNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitGroestlcoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Groestlcoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 150000,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
124
NBXplorer.Client/NBXplorerNetworkProvider.Liquid.cs
Normal file
124
NBXplorer.Client/NBXplorerNetworkProvider.Liquid.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
public class LiquidNBXplorerNetwork : NBXplorerNetwork
|
||||
{
|
||||
internal LiquidNBXplorerNetwork(INetworkSet networkSet, ChainName networkType) : base(networkSet, networkType)
|
||||
{
|
||||
}
|
||||
|
||||
internal override DerivationStrategyFactory CreateStrategyFactory()
|
||||
{
|
||||
var factory = base.CreateStrategyFactory();
|
||||
factory.AuthorizedOptions.Add("unblinded");
|
||||
factory.AuthorizedOptions.Add("slip77");
|
||||
return factory;
|
||||
}
|
||||
|
||||
public BitcoinAddress BlindIfNeeded(DerivationStrategyBase derivationStrategy, BitcoinAddress address, KeyPath keyPath)
|
||||
{
|
||||
if (derivationStrategy.Unblinded() || address is BitcoinBlindedAddress)
|
||||
return address;
|
||||
var blindingPubKey = GenerateBlindingKey(derivationStrategy, keyPath, address.ScriptPubKey, NBitcoinNetwork).PubKey;
|
||||
return new BitcoinBlindedAddress(blindingPubKey, address);
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public override BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
|
||||
{
|
||||
var addr = scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
|
||||
return BlindIfNeeded(derivationStrategy, addr, keyPath);
|
||||
}
|
||||
|
||||
public static Key GenerateSlip77BlindingKeyFromMnemonic(Mnemonic mnemonic, Script script)
|
||||
{
|
||||
var seed = mnemonic.DeriveSeed();
|
||||
var slip21 = Slip21Node.FromSeed(seed);
|
||||
var slip77 = slip21.GetSlip77Node();
|
||||
return slip77.DeriveSlip77BlindingKey(script);
|
||||
}
|
||||
|
||||
public static Key GenerateSlip77BlindingKeyFromMasterBlindingKey(Key masterBlindingKey, Script script)
|
||||
{
|
||||
return new Key(Hashes.HMACSHA256(masterBlindingKey.ToBytes(), script.ToBytes()));
|
||||
}
|
||||
|
||||
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey, Network network)
|
||||
{
|
||||
if (derivationStrategy.Unblinded())
|
||||
{
|
||||
throw new InvalidOperationException("This derivation scheme is set to only track unblinded addresses");
|
||||
}
|
||||
|
||||
if (derivationStrategy.Slip77(out var key))
|
||||
{
|
||||
if (HexEncoder.IsWellFormed(key))
|
||||
{
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), scriptPubKey);
|
||||
}
|
||||
try
|
||||
{
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), scriptPubKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = new Mnemonic(key);
|
||||
return GenerateSlip77BlindingKeyFromMnemonic(data, scriptPubKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The key provided for slip77 derivation was invalid.");
|
||||
}
|
||||
else if (derivationStrategy is StandardDerivationStrategyBase kpd && keyPath is not null)
|
||||
{
|
||||
var blindingKey = new Key(kpd.GetDerivation(keyPath.Derive(new KeyPath(0))).ScriptPubKey.WitHash.ToBytes());
|
||||
return blindingKey;
|
||||
}
|
||||
throw new InvalidOperationException("-[blinded] doesn't work on miniscript derivation strategies, use [slip77=key] instead");
|
||||
}
|
||||
}
|
||||
private void InitLiquid(ChainName networkType)
|
||||
{
|
||||
Add(new LiquidNBXplorerNetwork(NBitcoin.Altcoins.Liquid.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 150000,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetLBTC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Liquid.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LiquidDerivationStrategyOptionsExtensions
|
||||
{
|
||||
public static bool Unblinded(this DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
return derivationStrategyBase.AdditionalOptions.TryGetValue("unblinded", out _);
|
||||
}
|
||||
public static bool Slip77(this DerivationStrategyBase derivationStrategyBase ,out string key)
|
||||
{
|
||||
return derivationStrategyBase.AdditionalOptions.TryGetValue("slip77", out key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,15 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitLitecoin(NetworkType networkType)
|
||||
private void InitLitecoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Litecoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Monacoin.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Monacoin.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitMonacoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Monacoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetMONA()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Monacoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
NBXplorer.Client/NBXplorerNetworkProvider.MonetaryUnit.cs
Normal file
20
NBXplorer.Client/NBXplorerNetworkProvider.MonetaryUnit.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitMonetaryUnit(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.MonetaryUnit.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 70702
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetMUE()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.MonetaryUnit.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Monoeci.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Monoeci.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitMonoeci(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Monoeci.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 120203,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1998'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetXMCC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Monoeci.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
NBXplorer.Client/NBXplorerNetworkProvider.Pepecoin.cs
Normal file
24
NBXplorer.Client/NBXplorerNetworkProvider.Pepecoin.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitPepecoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Pepecoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 10000,
|
||||
ChainLoadingTimeout = TimeSpan.FromHours(1),
|
||||
ChainCacheLoadingTimeout = TimeSpan.FromMinutes(2),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3434'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetPEPE()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Pepecoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,15 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitPolis(NetworkType networkType)
|
||||
private void InitPolis(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Polis.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 1030000
|
||||
MinRPCVersion = 1030000,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Qtum.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Qtum.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitQtum(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Qtum.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("2301'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetQTUM()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Qtum.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Terracoin.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Terracoin.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitTerracoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Terracoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 120204,
|
||||
CoinType = networkType == ChainName.Mainnet ? new KeyPath("83'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetTRC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Terracoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Ufo.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Ufo.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitUfo(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Ufo.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 150000,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetUFO()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Ufo.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
NBXplorer.Client/NBXplorerNetworkProvider.Viacoin.cs
Normal file
21
NBXplorer.Client/NBXplorerNetworkProvider.Viacoin.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitViacoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Viacoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 140200,
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetVIA()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Viacoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,44 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
public NBXplorerNetworkProvider(NetworkType networkType)
|
||||
public NBXplorerNetworkProvider(ChainName networkType)
|
||||
{
|
||||
NetworkType = networkType;
|
||||
InitArgoneum(networkType);
|
||||
InitBitcoin(networkType);
|
||||
InitBitcore(networkType);
|
||||
InitLitecoin(networkType);
|
||||
InitDogecoin(networkType);
|
||||
InitPepecoin(networkType);
|
||||
InitBCash(networkType);
|
||||
InitGroestlcoin(networkType);
|
||||
InitBGold(networkType);
|
||||
InitDash(networkType);
|
||||
InitTerracoin(networkType);
|
||||
InitPolis(networkType);
|
||||
NetworkType = networkType;
|
||||
foreach(var chain in _Networks.Values)
|
||||
InitMonacoin(networkType);
|
||||
InitFeathercoin(networkType);
|
||||
InitUfo(networkType);
|
||||
InitViacoin(networkType);
|
||||
InitMonoeci(networkType);
|
||||
InitGobyte(networkType);
|
||||
InitColossus(networkType);
|
||||
InitChaincoin(networkType);
|
||||
InitLiquid(networkType);
|
||||
InitQtum(networkType);
|
||||
InitAlthash(networkType);
|
||||
InitMonetaryUnit(networkType);
|
||||
foreach (var chain in _Networks.Values)
|
||||
{
|
||||
chain.DerivationStrategyFactory = new DerivationStrategy.DerivationStrategyFactory(chain.NBitcoinNetwork);
|
||||
chain.DerivationStrategyFactory ??= chain.CreateStrategyFactory();
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkType NetworkType
|
||||
public ChainName NetworkType
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
@ -31,7 +46,7 @@ namespace NBXplorer
|
||||
|
||||
public NBXplorerNetwork GetFromCryptoCode(string cryptoCode)
|
||||
{
|
||||
_Networks.TryGetValue(cryptoCode, out NBXplorerNetwork network);
|
||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out NBXplorerNetwork network);
|
||||
return network;
|
||||
}
|
||||
|
||||
@ -43,6 +58,8 @@ namespace NBXplorer
|
||||
Dictionary<string, NBXplorerNetwork> _Networks = new Dictionary<string, NBXplorerNetwork>();
|
||||
private void Add(NBXplorerNetwork network)
|
||||
{
|
||||
if (network.NBitcoinNetwork == null)
|
||||
return;
|
||||
_Networks.Add(network.CryptoCode, network);
|
||||
}
|
||||
}
|
||||
|
||||
128
NBXplorer.Client/NBitcoinNBXplorerExtensions.cs
Normal file
128
NBXplorer.Client/NBitcoinNBXplorerExtensions.cs
Normal file
@ -0,0 +1,128 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace NBitcoin;
|
||||
|
||||
public static class NBitcoinNBXplorerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the keys</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
|
||||
RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.HDKeysFor(hd, accountKey, accountKeyPath);
|
||||
return Array.Empty<PSBTHDKeyMatch>();
|
||||
}
|
||||
/// <summary>
|
||||
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the keys</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> HDKeysFor(psbt, derivationStrategy, accountKey, null);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="coin">The coins to get the keys from</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
|
||||
RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return coin.HDKeysFor(hd, accountKey, accountKeyPath);
|
||||
return Array.Empty<PSBTHDKeyMatch>();
|
||||
}
|
||||
|
||||
static IHDScriptPubKey? ToHDScriptPubKey(DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
{
|
||||
if (derivationStrategy is null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
if (derivationStrategy is StandardDerivationStrategyBase standard)
|
||||
return standard;
|
||||
#if !NO_RECORD
|
||||
else if (derivationStrategy is PolicyDerivationStrategy policy && policy.GetHDScriptPubKey(accountKey) is IHDScriptPubKey hd)
|
||||
return hd;
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="coin">The coins to get the keys from</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> HDKeysFor(coin, derivationStrategy, accountKey, null);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the balance change if you were signing this transaction.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the balance</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>The balance change</returns>
|
||||
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.GetBalance(hd, accountKey, accountKeyPath);
|
||||
return Money.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the balance change if you were signing this transaction.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the balance</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>The balance change</returns>
|
||||
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> GetBalance(psbt, derivationStrategy, accountKey, null);
|
||||
|
||||
/// <summary>
|
||||
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT to sign</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key with which to sign</param>
|
||||
/// <param name="accountKeyPath">The account key path (eg. [masterFP]/49'/0'/0')</param>
|
||||
/// <returns>The signed PSBT</returns>
|
||||
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.SignAll(hd, accountKey, accountKeyPath);
|
||||
return psbt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT to sign</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key with which to sign</param>
|
||||
/// <returns>The signed PSBT</returns>
|
||||
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> SignAll(psbt, derivationStrategy, accountKey, null);
|
||||
}
|
||||
16
NBXplorer.Client/NotificationSessionBase.cs
Normal file
16
NBXplorer.Client/NotificationSessionBase.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using NBXplorer.Models;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public abstract class NotificationSessionBase
|
||||
{
|
||||
public NewEventBase NextEvent(CancellationToken cancellation = default)
|
||||
{
|
||||
return NextEventAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public abstract Task<NewEventBase> NextEventAsync(CancellationToken cancellation = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
rm "bin\release\" -Recurse -Force
|
||||
dotnet pack --configuration Release
|
||||
dotnet nuget push "bin\Release\" --source "https://api.nuget.org/v3/index.json"
|
||||
dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg
|
||||
$package=(ls .\bin\Release\*.nupkg).FullName
|
||||
dotnet nuget push $package --source "https://api.nuget.org/v3/index.json"
|
||||
$ver = ((ls .\bin\release\*.nupkg)[0].Name -replace 'NBXplorer\.Client\.(\d+(\.\d+){1,3}).*', '$1')
|
||||
git tag -a "Client/v$ver" -m "Client/$ver"
|
||||
git push --tags
|
||||
git push origin "Client/v$ver"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user