Compare commits
1504 Commits
v3.0.0-dev
...
simon/e2e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79fce5a269 | ||
|
|
05e88a25be | ||
|
|
574a800520 | ||
|
|
1b4bbc24ba | ||
|
|
0e5698955f | ||
|
|
e144e139b7 | ||
|
|
07a0850f99 | ||
|
|
72a5f4a521 | ||
|
|
04605e10a5 | ||
|
|
407ce3c76d | ||
|
|
a99278320b | ||
|
|
367ee68fe0 | ||
|
|
1038b4f908 | ||
|
|
e41829d5e0 | ||
|
|
156bf12034 | ||
|
|
fc1cdbac22 | ||
|
|
5c839aced3 | ||
|
|
3345a60863 | ||
|
|
36844f5b70 | ||
|
|
46d99db167 | ||
|
|
76dcb0830a | ||
|
|
0b29b61e93 | ||
|
|
f4425ed7fe | ||
|
|
7c02eab630 | ||
|
|
8867ef9680 | ||
|
|
1048a0ea83 | ||
|
|
7b216f7ec7 | ||
|
|
32cc2f0c63 | ||
|
|
c6b0e5ff0e | ||
|
|
68a580466f | ||
|
|
bb06a66a03 | ||
|
|
ec6372464f | ||
|
|
c13b8029d3 | ||
|
|
88e92b1b34 | ||
|
|
b0fa3efbbb | ||
|
|
0ccb5618f6 | ||
|
|
556313a676 | ||
|
|
1fe08a9ecc | ||
|
|
a11116ff3a | ||
|
|
ede0ccfd2e | ||
|
|
b64caf8f4b | ||
|
|
99290a7946 | ||
|
|
2d9709a427 | ||
|
|
a76d6c2949 | ||
|
|
d8cfc6e32d | ||
|
|
9457032ab1 | ||
|
|
74998e7f56 | ||
|
|
db0f968749 | ||
|
|
a1968e01e5 | ||
|
|
c7ab6b03fb | ||
|
|
2b206a7bcc | ||
|
|
c4b90c2a18 | ||
|
|
2a7d5760e0 | ||
|
|
c8719f1f1e | ||
|
|
d199c7746e | ||
|
|
67eb815992 | ||
|
|
4ccd3c8525 | ||
|
|
fc5af24b68 | ||
|
|
b17b66a52f | ||
|
|
819dedbc88 | ||
|
|
8717051a06 | ||
|
|
3e78b636d6 | ||
|
|
75316e60d0 | ||
|
|
31469ad361 | ||
|
|
485a58e453 | ||
|
|
92090399cc | ||
|
|
893c3cd87d | ||
|
|
c93159414c | ||
|
|
b2d4f4078f | ||
|
|
be17e45785 | ||
|
|
dbe774cc64 | ||
|
|
64223c4744 | ||
|
|
07fd3d3409 | ||
|
|
f7360433a1 | ||
|
|
f6fac92180 | ||
|
|
82d1502bfa | ||
|
|
8ab104d191 | ||
|
|
263742132c | ||
|
|
e3c3f31ddb | ||
|
|
70d1f52268 | ||
|
|
e44f30d7a7 | ||
|
|
099d84a395 | ||
|
|
12285465d9 | ||
|
|
eab008c707 | ||
|
|
0f1def5822 | ||
|
|
fad39e0bea | ||
|
|
0a5a2c3c7e | ||
|
|
b526ee0877 | ||
|
|
98d98b2478 | ||
|
|
3bea10ea35 | ||
|
|
1ea45e9e96 | ||
|
|
624b3473fc | ||
|
|
a3e61525fe | ||
|
|
9e05060af4 | ||
|
|
ee53597fe9 | ||
|
|
e362003746 | ||
|
|
185eae00e9 | ||
|
|
8c2d0e1d6c | ||
|
|
009fba3dd5 | ||
|
|
a0fc4861d4 | ||
|
|
62085581dd | ||
|
|
05efa8c300 | ||
|
|
eee99a6407 | ||
|
|
98cee4a6cd | ||
|
|
0302999806 | ||
|
|
1876d67e74 | ||
|
|
c0bb75e5b1 | ||
|
|
4059a902a1 | ||
|
|
4cc19a7235 | ||
|
|
c874d642c5 | ||
|
|
f0af1703da | ||
|
|
b47e148677 | ||
|
|
d22743c4b8 | ||
|
|
5452d7f524 | ||
|
|
ff9e2456b9 | ||
|
|
4e581347c8 | ||
|
|
820777236e | ||
|
|
beeb5eb08c | ||
|
|
b78aca0282 | ||
|
|
9572f2d554 | ||
|
|
e59308c2f5 | ||
|
|
ef13596b59 | ||
|
|
c7f48b4390 | ||
|
|
80da024bbb | ||
|
|
f75f85f914 | ||
|
|
b3ac107b0b | ||
|
|
f8cedaa7a3 | ||
|
|
72bb92dd8b | ||
|
|
e3c4e219f3 | ||
|
|
aa3fa4478a | ||
|
|
c9171224e1 | ||
|
|
248cef7718 | ||
|
|
26c03eee88 | ||
|
|
db10ab9aae | ||
|
|
2ee7b9531a | ||
|
|
5f6af83944 | ||
|
|
8d2204a53f | ||
|
|
96bec279a9 | ||
|
|
5178ae43f6 | ||
|
|
ca26154426 | ||
|
|
021f0b32a1 | ||
|
|
b8cfeb579b | ||
|
|
fc5b99f93f | ||
|
|
ce4b0ed0f3 | ||
|
|
a31729b8b8 | ||
|
|
fbf27560b3 | ||
|
|
79e494150c | ||
|
|
b1a43abc0e | ||
|
|
3e50a3c9e7 | ||
|
|
104c7f4285 | ||
|
|
132d6204c3 | ||
|
|
77c6ad5576 | ||
|
|
4d35845c18 | ||
|
|
3d8a4a85f7 | ||
|
|
1545347a45 | ||
|
|
9facf28ba5 | ||
|
|
d6eb98561b | ||
|
|
0f688e8347 | ||
|
|
7f53741a7b | ||
|
|
d5672691e1 | ||
|
|
e6049c707b | ||
|
|
91e74e769c | ||
|
|
7f252f06b7 | ||
|
|
1b9d3f669d | ||
|
|
ef0ba9a77a | ||
|
|
15f10736e2 | ||
|
|
924399df46 | ||
|
|
cddff129b3 | ||
|
|
7c90e8ae06 | ||
|
|
34d996c7cb | ||
|
|
641a2ae3ae | ||
|
|
11a849ef28 | ||
|
|
7b0347e846 | ||
|
|
bb1352ed58 | ||
|
|
fa6456b92c | ||
|
|
bfea19238b | ||
|
|
36b91cfdfd | ||
|
|
7b56212064 | ||
|
|
d4be3c2c4c | ||
|
|
3a9f06f651 | ||
|
|
e652eb339d | ||
|
|
96435c329f | ||
|
|
1c69613d65 | ||
|
|
3707763e30 | ||
|
|
b6ce8229f0 | ||
|
|
b62ae9b6f6 | ||
|
|
f61ace2f92 | ||
|
|
2b572f2494 | ||
|
|
c0e4c1efe1 | ||
|
|
8078caaa89 | ||
|
|
5eb117165f | ||
|
|
a2dcf0d545 | ||
|
|
212d58f917 | ||
|
|
d1eec80afb | ||
|
|
05c6709926 | ||
|
|
f1a48db9ee | ||
|
|
76ce43d289 | ||
|
|
51f5b728f3 | ||
|
|
8fa1863aff | ||
|
|
e3d1d9c1c0 | ||
|
|
f2f8d91e10 | ||
|
|
11ef090846 | ||
|
|
5cacd2635e | ||
|
|
1b4780c25b | ||
|
|
5ea44f2e7d | ||
|
|
261c794817 | ||
|
|
de9fae5cd7 | ||
|
|
439c52af30 | ||
|
|
4dbcd6ca18 | ||
|
|
2921c94520 | ||
|
|
ab4a258be3 | ||
|
|
21b0d50947 | ||
|
|
233ec112c2 | ||
|
|
6f0a5c9b44 | ||
|
|
2bc243b115 | ||
|
|
154f8e65a7 | ||
|
|
cc9855aa65 | ||
|
|
7b45d922bc | ||
|
|
9488ca50a3 | ||
|
|
9f559248cc | ||
|
|
067aac4d06 | ||
|
|
37eb17cc22 | ||
|
|
c7382a1c6c | ||
|
|
f782693b26 | ||
|
|
908988d06e | ||
|
|
a0c21e4120 | ||
|
|
bd96cbd701 | ||
|
|
9cccdcf70f | ||
|
|
2489fc4902 | ||
|
|
e378df4158 | ||
|
|
fa5f758875 | ||
|
|
a46f15e7ec | ||
|
|
46ecd2a51f | ||
|
|
c1aaf2f61a | ||
|
|
d2b57c8d4f | ||
|
|
f27a5700fa | ||
|
|
17522cca6e | ||
|
|
3f2581886d | ||
|
|
c5beee3a40 | ||
|
|
94e223e8da | ||
|
|
416e1bb4cb | ||
|
|
564dc08509 | ||
|
|
b9334c93f5 | ||
|
|
67761230e3 | ||
|
|
7cc01af631 | ||
|
|
96e2e6060b | ||
|
|
0723778e7c | ||
|
|
39d7e4ac3e | ||
|
|
1869368a49 | ||
|
|
b83f19f186 | ||
|
|
b248aad6d9 | ||
|
|
87c9f920c1 | ||
|
|
ea0e339d60 | ||
|
|
0a0b5e52fe | ||
|
|
5314f35974 | ||
|
|
87d2f6cf90 | ||
|
|
4266bdcbb6 | ||
|
|
73e68534f1 | ||
|
|
ee5b383a46 | ||
|
|
fa4b445dca | ||
|
|
06eaadd517 | ||
|
|
ebdc1dbf6d | ||
|
|
184903670a | ||
|
|
878561244a | ||
|
|
a0e9b33199 | ||
|
|
0a4513c8fb | ||
|
|
77b9277da5 | ||
|
|
009fac183f | ||
|
|
83a6df4d04 | ||
|
|
474d384b0c | ||
|
|
5870782abf | ||
|
|
fb335f62db | ||
|
|
346ef0028d | ||
|
|
916cae2a8f | ||
|
|
550e667a2f | ||
|
|
eb4ebf6786 | ||
|
|
7515348997 | ||
|
|
41441bb958 | ||
|
|
98c49918f7 | ||
|
|
5855e09663 | ||
|
|
558b876d47 | ||
|
|
fb63af5070 | ||
|
|
d02a2ccf65 | ||
|
|
e3bb812203 | ||
|
|
e3af4ea61c | ||
|
|
7d10b32861 | ||
|
|
adea897e93 | ||
|
|
17ec50dba0 | ||
|
|
53a36d042f | ||
|
|
7531a53b2e | ||
|
|
d59bc085e5 | ||
|
|
5d37e08c64 | ||
|
|
b5d89b83fa | ||
|
|
1245673575 | ||
|
|
62a72755c7 | ||
|
|
5b8ec8925a | ||
|
|
262866c15b | ||
|
|
aec7eb57c2 | ||
|
|
8d3b4733f5 | ||
|
|
3e4debdf7a | ||
|
|
b719b76999 | ||
|
|
6adbda5185 | ||
|
|
01311d0ba1 | ||
|
|
6081daacef | ||
|
|
54c9970386 | ||
|
|
1e4a599055 | ||
|
|
b051861d61 | ||
|
|
3c7deafffd | ||
|
|
845123fc63 | ||
|
|
d3e3650cac | ||
|
|
008cc385da | ||
|
|
817a6bef6e | ||
|
|
8dd8fe5fb1 | ||
|
|
c734a81f08 | ||
|
|
a0f4c260ab | ||
|
|
000a989055 | ||
|
|
90331e2c1b | ||
|
|
78ac0137b3 | ||
|
|
3d9133c47e | ||
|
|
481859bc8f | ||
|
|
811feec145 | ||
|
|
b3e59c06e9 | ||
|
|
67d44e3d6f | ||
|
|
ca4b1943a8 | ||
|
|
df0f244bd1 | ||
|
|
fe1ad86885 | ||
|
|
aee2454a98 | ||
|
|
5c814d9c22 | ||
|
|
2c5d7fbc9f | ||
|
|
570f7841ce | ||
|
|
117c066425 | ||
|
|
96f9f66e7f | ||
|
|
ce2742ff9c | ||
|
|
4547a2757c | ||
|
|
4d44ee55fc | ||
|
|
29875e0095 | ||
|
|
bc498733fc | ||
|
|
dc09e75783 | ||
|
|
544261eafe | ||
|
|
58c0c060d5 | ||
|
|
af7a962a0b | ||
|
|
7b3cc6372b | ||
|
|
b49a6c4cac | ||
|
|
b0db348605 | ||
|
|
b1aa4f50bd | ||
|
|
301f1821ae | ||
|
|
b9a053387f | ||
|
|
82c271267a | ||
|
|
06affa60cc | ||
|
|
4ef4e5b98a | ||
|
|
6bc52dcc82 | ||
|
|
1f357408ac | ||
|
|
a7be59df3e | ||
|
|
82360f5525 | ||
|
|
8762ccaa09 | ||
|
|
3f7a24fb52 | ||
|
|
09b09710e4 | ||
|
|
08d3beed72 | ||
|
|
920f225e6c | ||
|
|
9c2d010516 | ||
|
|
743c7e8bfb | ||
|
|
8116b50d50 | ||
|
|
df977926e2 | ||
|
|
b0fac806d0 | ||
|
|
398593828f | ||
|
|
9aac0ddce7 | ||
|
|
79eb9635c2 | ||
|
|
41c373c39d | ||
|
|
27374bd131 | ||
|
|
8c07e3c31a | ||
|
|
84ba721407 | ||
|
|
cdaf42797f | ||
|
|
0a116804e8 | ||
|
|
68edf4306c | ||
|
|
61bbb95819 | ||
|
|
3fa32edf25 | ||
|
|
db220d9dfd | ||
|
|
bf61557879 | ||
|
|
ebaf5cd304 | ||
|
|
51154d3954 | ||
|
|
41b4b2eddf | ||
|
|
0dff7e82a3 | ||
|
|
9cba7ccf75 | ||
|
|
6177b97bd1 | ||
|
|
f7f1a99486 | ||
|
|
6b955acf9e | ||
|
|
530610add6 | ||
|
|
428f9369e2 | ||
|
|
a9defb21bb | ||
|
|
680e9562a0 | ||
|
|
66c5c303b3 | ||
|
|
5a86c8c83a | ||
|
|
725e9c0d95 | ||
|
|
74e59d6ea5 | ||
|
|
9ac45a6cc3 | ||
|
|
94d537daa6 | ||
|
|
30bc026c28 | ||
|
|
21942f8ab1 | ||
|
|
147f55fec3 | ||
|
|
e73f2cbdc1 | ||
|
|
009e18b622 | ||
|
|
5f20803e21 | ||
|
|
832d16cf2d | ||
|
|
5e8b5e75d8 | ||
|
|
c5ef1011d8 | ||
|
|
6a14043641 | ||
|
|
0f526f24cb | ||
|
|
4426bb10a9 | ||
|
|
18a7859cca | ||
|
|
349d491f7d | ||
|
|
7556424f0b | ||
|
|
1d827a9724 | ||
|
|
f019dd67b3 | ||
|
|
8b27ac1bbf | ||
|
|
b91774d50c | ||
|
|
22a5cd2de2 | ||
|
|
e5489277c6 | ||
|
|
04b6bee8a1 | ||
|
|
6cd8cf660b | ||
|
|
1b6fd29c82 | ||
|
|
a31dae67a8 | ||
|
|
76e3053207 | ||
|
|
985b7577e4 | ||
|
|
c748e5cda9 | ||
|
|
a99f45cd47 | ||
|
|
de1d7839b3 | ||
|
|
6aa3e38af2 | ||
|
|
dca7df709b | ||
|
|
e40e9f7d11 | ||
|
|
285bb357ba | ||
|
|
c3b9828d42 | ||
|
|
871e590305 | ||
|
|
0e5a1abb2b | ||
|
|
5665c6e6ec | ||
|
|
06b696f0bb | ||
|
|
75ca963bd5 | ||
|
|
3a6647eac0 | ||
|
|
9ad6b925c8 | ||
|
|
17720b98c1 | ||
|
|
5bb3e930cc | ||
|
|
347bddc974 | ||
|
|
4eca8240db | ||
|
|
1c135b4c67 | ||
|
|
f24223ca06 | ||
|
|
9748aa05cf | ||
|
|
1a5613bf65 | ||
|
|
e55e4e378a | ||
|
|
927eb98072 | ||
|
|
99ea1ad0a0 | ||
|
|
fed3012449 | ||
|
|
bbff50527b | ||
|
|
4470461a98 | ||
|
|
645fd98c30 | ||
|
|
10de603ee7 | ||
|
|
685c1c9fb2 | ||
|
|
d02a67766d | ||
|
|
7721fde7b6 | ||
|
|
aa10d1233c | ||
|
|
ba79821aac | ||
|
|
e054e1d5a3 | ||
|
|
565910f9f9 | ||
|
|
2915be8fd6 | ||
|
|
ff25b8ff1e | ||
|
|
2d03ab6346 | ||
|
|
a530b70f9f | ||
|
|
986d71d47f | ||
|
|
79f4720516 | ||
|
|
6135b1db10 | ||
|
|
4269077d4b | ||
|
|
da0df70ad2 | ||
|
|
6253d3716d | ||
|
|
614432426a | ||
|
|
e51951c3ff | ||
|
|
b38bf0f7b6 | ||
|
|
503de93094 | ||
|
|
58f3169712 | ||
|
|
53da6549e2 | ||
|
|
65046c4cb8 | ||
|
|
9416fd25f4 | ||
|
|
adde1a86e4 | ||
|
|
8a96669260 | ||
|
|
92434d41a4 | ||
|
|
2c81ebb637 | ||
|
|
7735da96f2 | ||
|
|
d914df20ba | ||
|
|
852e2b2fa0 | ||
|
|
9396a4bbae | ||
|
|
bf95938be8 | ||
|
|
8d2e7bef7a | ||
|
|
6f31fb2a08 | ||
|
|
34b5678199 | ||
|
|
432496d2a0 | ||
|
|
23ee613414 | ||
|
|
c391a532de | ||
|
|
cd56128bb6 | ||
|
|
07370a8dc7 | ||
|
|
bf51e3e1c9 | ||
|
|
eec6efcc22 | ||
|
|
64dd55b44b | ||
|
|
e2d2a8da26 | ||
|
|
487d82eccf | ||
|
|
c43b567847 | ||
|
|
5d9c846a8f | ||
|
|
cc30536857 | ||
|
|
8625419417 | ||
|
|
a9341821c5 | ||
|
|
d074ff1d4c | ||
|
|
9837a69a1a | ||
|
|
32eaf29aaa | ||
|
|
5316d1705a | ||
|
|
2fb735c430 | ||
|
|
5001d553f3 | ||
|
|
663a09ea97 | ||
|
|
8afdd9a482 | ||
|
|
fc12733132 | ||
|
|
0c200e090d | ||
|
|
f4a9aeacc7 | ||
|
|
1a2487b740 | ||
|
|
3425bdd100 | ||
|
|
be72a26760 | ||
|
|
77cd07cc93 | ||
|
|
0e122c15e2 | ||
|
|
2ec0e6634b | ||
|
|
a0992f6091 | ||
|
|
b8820684c3 | ||
|
|
7c08a104ce | ||
|
|
fb8bd4b194 | ||
|
|
20d948c280 | ||
|
|
1710ae0503 | ||
|
|
8735b62510 | ||
|
|
4cd70941f7 | ||
|
|
2773c21343 | ||
|
|
54763fe5d6 | ||
|
|
99b8a3cb3e | ||
|
|
d15e2ada42 | ||
|
|
70548ed532 | ||
|
|
c85e7b08c3 | ||
|
|
bdd51a0f4b | ||
|
|
769bb6f1be | ||
|
|
2fc89f6a35 | ||
|
|
f8447c10d5 | ||
|
|
3d4316cd44 | ||
|
|
6ed6f2e2cf | ||
|
|
2b96c99fb3 | ||
|
|
6b481d5a07 | ||
|
|
cc77476756 | ||
|
|
736833b4f6 | ||
|
|
c37858fa54 | ||
|
|
fb44c1d8a8 | ||
|
|
815dcbd4ce | ||
|
|
3c6e18f198 | ||
|
|
fa84283a01 | ||
|
|
46c4d57367 | ||
|
|
7dfb3c452f | ||
|
|
9be56badee | ||
|
|
bbdc9e4aa4 | ||
|
|
a3e58d632e | ||
|
|
df7e647523 | ||
|
|
c9edfa1826 | ||
|
|
9ebb98b1b9 | ||
|
|
cc5ccd01e2 | ||
|
|
c34be2a334 | ||
|
|
1270a2d67a | ||
|
|
5a4b79b83e | ||
|
|
2fc0079530 | ||
|
|
680d8504b6 | ||
|
|
89db3dc70e | ||
|
|
9d6816132b | ||
|
|
f496fc9653 | ||
|
|
9318aa9a6a | ||
|
|
db3db49fbc | ||
|
|
75ad6a2335 | ||
|
|
ec209bb618 | ||
|
|
2b21ddd0b2 | ||
|
|
d0358f1551 | ||
|
|
6ce1970ef4 | ||
|
|
6597854b14 | ||
|
|
69cd054a97 | ||
|
|
0ea22961e8 | ||
|
|
1ce72e23a3 | ||
|
|
140c371c51 | ||
|
|
39e55bb3f8 | ||
|
|
5a9dde0807 | ||
|
|
ae8b6043b2 | ||
|
|
b49618fbed | ||
|
|
46e8f6137c | ||
|
|
ec2ab174de | ||
|
|
4e18ff3329 | ||
|
|
90a8ff47b7 | ||
|
|
c4f5aa1874 | ||
|
|
3c106a3c8f | ||
|
|
ec033a9eaf | ||
|
|
6b0496029c | ||
|
|
642bf86423 | ||
|
|
3e07d6b684 | ||
|
|
3a94687548 | ||
|
|
8028d80ab8 | ||
|
|
453bcffbbc | ||
|
|
c6b2db9282 | ||
|
|
00fb261124 | ||
|
|
11113041bb | ||
|
|
63cb6c3804 | ||
|
|
2ff3d00bd7 | ||
|
|
f0a63aaba3 | ||
|
|
44ef81fde0 | ||
|
|
16caae8123 | ||
|
|
5a897e56ab | ||
|
|
5715915850 | ||
|
|
53109aa50a | ||
|
|
d52ca35cc0 | ||
|
|
d00f4245f8 | ||
|
|
4723ca88ec | ||
|
|
e5d23e8076 | ||
|
|
2827dcd0ba | ||
|
|
7d7f9b1665 | ||
|
|
2eb9108046 | ||
|
|
b198528592 | ||
|
|
7dcd952a40 | ||
|
|
89a3b1c577 | ||
|
|
3dbbc83077 | ||
|
|
011a854a84 | ||
|
|
6261f83e5e | ||
|
|
c4b45180dd | ||
|
|
69b40cf073 | ||
|
|
9ef79a268d | ||
|
|
75c9e15e16 | ||
|
|
dfede7fe25 | ||
|
|
a86709d7b0 | ||
|
|
396eee3555 | ||
|
|
5067c88642 | ||
|
|
e35ac6e1a2 | ||
|
|
5b93c8e875 | ||
|
|
c71a0afe1f | ||
|
|
2d12d2e5ef | ||
|
|
23fa28567d | ||
|
|
a624e82630 | ||
|
|
da4c2f5307 | ||
|
|
b91f195955 | ||
|
|
69b346ab00 | ||
|
|
1c89a1a44e | ||
|
|
3088befbf5 | ||
|
|
7ed35b955d | ||
|
|
e7e4b63fbc | ||
|
|
723ac4cece | ||
|
|
1a91f2b0a3 | ||
|
|
300bfd225b | ||
|
|
cf09669902 | ||
|
|
d5525ae324 | ||
|
|
ff8b0a8d80 | ||
|
|
8fc5fdbde6 | ||
|
|
27c70bd919 | ||
|
|
7432e6e29b | ||
|
|
a9c3637c7f | ||
|
|
de95dd9c77 | ||
|
|
da1e5c515e | ||
|
|
ce879152fd | ||
|
|
d76490df0c | ||
|
|
a80372f335 | ||
|
|
102625b3ea | ||
|
|
eb3c248acd | ||
|
|
9140bcb408 | ||
|
|
35d0e7fae7 | ||
|
|
f114a8ca75 | ||
|
|
0b663c1a77 | ||
|
|
c494207469 | ||
|
|
ce31d0512c | ||
|
|
3ecc8ae8cf | ||
|
|
1e820a0fc8 | ||
|
|
e3abdf4b4f | ||
|
|
caf7011df5 | ||
|
|
7caad9fca9 | ||
|
|
84e1ac31c2 | ||
|
|
3b4ac3b6b7 | ||
|
|
cfe5da2276 | ||
|
|
110b7a934c | ||
|
|
d059c5ca27 | ||
|
|
bf37affe47 | ||
|
|
2798b43913 | ||
|
|
425edb9b9f | ||
|
|
f68c8cc621 | ||
|
|
c5fc476834 | ||
|
|
776404dbde | ||
|
|
1067131120 | ||
|
|
6cf753ddaf | ||
|
|
277f8f7bfd | ||
|
|
c249da7901 | ||
|
|
3720d67666 | ||
|
|
84d4eaa932 | ||
|
|
d62300ccff | ||
|
|
48bdae4e78 | ||
|
|
193c41cb81 | ||
|
|
5872b2c46b | ||
|
|
e158c10688 | ||
|
|
b4e46c3ff8 | ||
|
|
254d962558 | ||
|
|
c75afe20cd | ||
|
|
98e9d1a6c3 | ||
|
|
c4577b8c09 | ||
|
|
95c4da51ed | ||
|
|
2e336d7ad1 | ||
|
|
903ff1ea66 | ||
|
|
f02d8e0626 | ||
|
|
ea04ea0048 | ||
|
|
473da82caa | ||
|
|
415ad3de70 | ||
|
|
d91c6bceed | ||
|
|
aa5355e93d | ||
|
|
9672928da9 | ||
|
|
d189e70817 | ||
|
|
d7acd389bf | ||
|
|
9fe44bd6ba | ||
|
|
4445fe408b | ||
|
|
790e76ab26 | ||
|
|
66a88b8422 | ||
|
|
bb91f9142e | ||
|
|
66f90cb0bd | ||
|
|
f1572f0038 | ||
|
|
c3963d6a0d | ||
|
|
1dd86df3e0 | ||
|
|
c8d443bea7 | ||
|
|
575fc737ca | ||
|
|
ebd4408b8d | ||
|
|
d6d8c85419 | ||
|
|
fbb409e17b | ||
|
|
b6d03953b9 | ||
|
|
d45104f7c9 | ||
|
|
d175c34e5b | ||
|
|
2bf2440e3a | ||
|
|
124c0acbe1 | ||
|
|
69c5a2fb5a | ||
|
|
c4f08e0d41 | ||
|
|
87ee14f189 | ||
|
|
122b4b05c4 | ||
|
|
09f7dddf14 | ||
|
|
f7ad45939c | ||
|
|
7b6246a035 | ||
|
|
a8d2138404 | ||
|
|
0b608c96dd | ||
|
|
a0402b92f9 | ||
|
|
14e05b43c7 | ||
|
|
fc8f8abc7e | ||
|
|
a1f1b09c55 | ||
|
|
ad2d7af084 | ||
|
|
9d3044efae | ||
|
|
bac21afa54 | ||
|
|
f98bb675e7 | ||
|
|
3e057f2db1 | ||
|
|
4fbdf92f0c | ||
|
|
fd60940a08 | ||
|
|
c54bc5a4bb | ||
|
|
04559e7b98 | ||
|
|
255919f03f | ||
|
|
b92b5cdd87 | ||
|
|
03036bf59d | ||
|
|
563def45d8 | ||
|
|
e4c9b67239 | ||
|
|
38bf056b6d | ||
|
|
91ddf7ea98 | ||
|
|
a9a1ff68ab | ||
|
|
dfc61f3991 | ||
|
|
f9d03b1bb4 | ||
|
|
868dac91c7 | ||
|
|
3c689e34b8 | ||
|
|
835f16aab6 | ||
|
|
2c2a6ee872 | ||
|
|
7d0720d55f | ||
|
|
c4dec53387 | ||
|
|
517e82ec8b | ||
|
|
0c72e1b6ed | ||
|
|
5d1877a275 | ||
|
|
8a43ed1a61 | ||
|
|
61c9debcca | ||
|
|
172fb0bf41 | ||
|
|
eedfbacf01 | ||
|
|
396b7eb3d3 | ||
|
|
05724b9d58 | ||
|
|
f67ae10684 | ||
|
|
e11ce14f81 | ||
|
|
833418514e | ||
|
|
6277813414 | ||
|
|
6936b97ba6 | ||
|
|
4dfabaf165 | ||
|
|
06f60df4cf | ||
|
|
29a8f6a09e | ||
|
|
9394572ec3 | ||
|
|
8e521a2376 | ||
|
|
b227767fee | ||
|
|
1c1c93abfc | ||
|
|
ec7c691044 | ||
|
|
92e6df1295 | ||
|
|
8b0015b3ff | ||
|
|
19ea077fe5 | ||
|
|
16502332fd | ||
|
|
7f2987f250 | ||
|
|
25e9741fc2 | ||
|
|
5be66f0b05 | ||
|
|
a517c6c711 | ||
|
|
43f35837da | ||
|
|
f9101b381b | ||
|
|
6b84dc2be4 | ||
|
|
8082e1d1cf | ||
|
|
36bc1db195 | ||
|
|
fa9a8bdba8 | ||
|
|
b44b790e28 | ||
|
|
cf8d179925 | ||
|
|
32db01d353 | ||
|
|
7c806b4b23 | ||
|
|
c581be0e97 | ||
|
|
e1e4e79b68 | ||
|
|
246ca593bb | ||
|
|
136af78147 | ||
|
|
da1ad1c316 | ||
|
|
2e893e0aea | ||
|
|
b41382dfee | ||
|
|
8d66374374 | ||
|
|
c00d2f3763 | ||
|
|
e7cba13704 | ||
|
|
55598e7974 | ||
|
|
bf81cc5ba9 | ||
|
|
c5b12e3bc3 | ||
|
|
762c5aa718 | ||
|
|
e95e64a443 | ||
|
|
d10fdaad46 | ||
|
|
5b554852bb | ||
|
|
ff8fb3b24f | ||
|
|
1219526e2d | ||
|
|
85006a5bec | ||
|
|
82e2f46eba | ||
|
|
0719b20110 | ||
|
|
25b510359f | ||
|
|
02eb633d89 | ||
|
|
522a473213 | ||
|
|
bc583979c5 | ||
|
|
3222e0efd2 | ||
|
|
f720c90c03 | ||
|
|
1bf5047377 | ||
|
|
bdeac877d2 | ||
|
|
1bcacf53be | ||
|
|
0c8d9daaec | ||
|
|
307d3627a0 | ||
|
|
db04c4663e | ||
|
|
a0a6a0da4f | ||
|
|
26968605cc | ||
|
|
ccd8412e6a | ||
|
|
272a2c8441 | ||
|
|
2ee656a176 | ||
|
|
5cecd9f8a7 | ||
|
|
c0ec9f70c3 | ||
|
|
84dae82e90 | ||
|
|
9dbf3b54fb | ||
|
|
ce46aae8cc | ||
|
|
fb621f9812 | ||
|
|
2156924d7e | ||
|
|
60a30aaede | ||
|
|
7dfdb5553e | ||
|
|
824bf5fc63 | ||
|
|
2b44055fc7 | ||
|
|
7bef8653b1 | ||
|
|
3b419be341 | ||
|
|
331b54fe89 | ||
|
|
9514bb703b | ||
|
|
746a045c48 | ||
|
|
684ad9f0e6 | ||
|
|
24b5d4e971 | ||
|
|
fda40cad48 | ||
|
|
2ce4b5604e | ||
|
|
fb660e8477 | ||
|
|
621def712d | ||
|
|
8382a27a7c | ||
|
|
b7d96a2a26 | ||
|
|
3149199c8a | ||
|
|
0c3ef4eabc | ||
|
|
fffcb5038f | ||
|
|
77d42bfdbb | ||
|
|
f840ac951b | ||
|
|
22a48efd19 | ||
|
|
fba3f7ec1c | ||
|
|
6b3005c49d | ||
|
|
17132ff047 | ||
|
|
355fe58b43 | ||
|
|
604b0ba3e6 | ||
|
|
d016838356 | ||
|
|
f77582250f | ||
|
|
976e505445 | ||
|
|
42c60fd991 | ||
|
|
9a838c7269 | ||
|
|
f31b28251c | ||
|
|
ced1595d70 | ||
|
|
0b0109d821 | ||
|
|
992da1e5d2 | ||
|
|
25c0eb62b2 | ||
|
|
9b9aaed757 | ||
|
|
b699063153 | ||
|
|
6947e19ca9 | ||
|
|
9d4bbe9317 | ||
|
|
5575798cb6 | ||
|
|
57cc53b64e | ||
|
|
a0d3afb4d2 | ||
|
|
67afda7dcf | ||
|
|
a56af00500 | ||
|
|
e3971af207 | ||
|
|
37725bb341 | ||
|
|
f17635193a | ||
|
|
1c73dc59f9 | ||
|
|
3adbba2959 | ||
|
|
ea1629fba8 | ||
|
|
87a4c087e5 | ||
|
|
692edea1ce | ||
|
|
11cfb8a783 | ||
|
|
0b953f21b0 | ||
|
|
d5508872dd | ||
|
|
321181d708 | ||
|
|
f3bd50d4ab | ||
|
|
12a843c386 | ||
|
|
21f91bcb6e | ||
|
|
d57bd56743 | ||
|
|
08969592ea | ||
|
|
f0437886ee | ||
|
|
cfedb5fd24 | ||
|
|
a9ad892495 | ||
|
|
00838ea947 | ||
|
|
7761ea53c6 | ||
|
|
aeeb4af9ba | ||
|
|
9186f664da | ||
|
|
83db2a3b72 | ||
|
|
3cfd54b4c5 | ||
|
|
c6db016c99 | ||
|
|
6f6a9ea1a4 | ||
|
|
83246be962 | ||
|
|
dcd94d868a | ||
|
|
e9fc5c0433 | ||
|
|
e281684ca4 | ||
|
|
6a915c0b88 | ||
|
|
078dc8d9a1 | ||
|
|
232f81b906 | ||
|
|
8701119304 | ||
|
|
33c9f4a8dc | ||
|
|
0654872627 | ||
|
|
cca798eeaa | ||
|
|
1498db3b33 | ||
|
|
05b022dec8 | ||
|
|
6c6c18830c | ||
|
|
46b5b26347 | ||
|
|
bd5abf6592 | ||
|
|
2c04896397 | ||
|
|
286fc8e9ad | ||
|
|
52fa6a0f78 | ||
|
|
a53b587395 | ||
|
|
2341b1d79e | ||
|
|
35215c7740 | ||
|
|
e2080e5548 | ||
|
|
60a07fb093 | ||
|
|
28477cc433 | ||
|
|
6bb2f06118 | ||
|
|
68bab5b7b8 | ||
|
|
6bc7eec4e3 | ||
|
|
4364b5fb06 | ||
|
|
86bc7b3f37 | ||
|
|
ea0a4d1d67 | ||
|
|
aa80fa550b | ||
|
|
bdbe833e2a | ||
|
|
c1f338774a | ||
|
|
e5ab8ab732 | ||
|
|
ce204d03c6 | ||
|
|
0e37e85af6 | ||
|
|
b0630de3cc | ||
|
|
4b3123b5ae | ||
|
|
2ff2b78d26 | ||
|
|
35ff2c7225 | ||
|
|
69786d5b4b | ||
|
|
3c3ab96164 | ||
|
|
0c49c5313b | ||
|
|
c5f6509a4c | ||
|
|
8bac2db12c | ||
|
|
0605e80d89 | ||
|
|
abad704fc6 | ||
|
|
d7459ba943 | ||
|
|
ec7da01186 | ||
|
|
2ce1cc24b9 | ||
|
|
cb95423178 | ||
|
|
a68c8d317c | ||
|
|
f58c2e6fe5 | ||
|
|
203e2904d6 | ||
|
|
38971d33a6 | ||
|
|
83f3d07538 | ||
|
|
9e844ffbbd | ||
|
|
0497c750be | ||
|
|
788caf05ce | ||
|
|
db34ca6a5f | ||
|
|
3bbdf6fb25 | ||
|
|
6b16b2d651 | ||
|
|
0279fe69d4 | ||
|
|
e1c47fddee | ||
|
|
6b2b3459f7 | ||
|
|
1d91e76ec2 | ||
|
|
e8dbd777d0 | ||
|
|
987def9baa | ||
|
|
d9197e28be | ||
|
|
72f4d811c1 | ||
|
|
f8144bd9a0 | ||
|
|
d6d56688ea | ||
|
|
97817e770f | ||
|
|
bbf8aed38f | ||
|
|
ef5c8ddcdf | ||
|
|
69e1d6150d | ||
|
|
86c3ef68e4 | ||
|
|
ec54ed6f94 | ||
|
|
c2a3ff4b67 | ||
|
|
cc9e4f2d43 | ||
|
|
4c62d54b6b | ||
|
|
8bbc27dae5 | ||
|
|
2ffaeb50df | ||
|
|
12d212b89f | ||
|
|
1cdb38af0b | ||
|
|
947f55dda2 | ||
|
|
c52bde140f | ||
|
|
1663637ed9 | ||
|
|
d24217282b | ||
|
|
b6a1547ce4 | ||
|
|
66f431d3d3 | ||
|
|
d02625eb0d | ||
|
|
a3ac9c31ed | ||
|
|
4729a87b39 | ||
|
|
16c154f39d | ||
|
|
fa98c73067 | ||
|
|
d32de4fa06 | ||
|
|
50ee7c07d1 | ||
|
|
2ac041dc6c | ||
|
|
4aac31332f | ||
|
|
9a6ac69983 | ||
|
|
c93aa4d82c | ||
|
|
16fe1f3cec | ||
|
|
42d591bf4c | ||
|
|
2133356047 | ||
|
|
d7b0923000 | ||
|
|
2fa33b749c | ||
|
|
90cc75479e | ||
|
|
e2eadd0433 | ||
|
|
1bd045582d | ||
|
|
c6e8885c94 | ||
|
|
5531d04f10 | ||
|
|
85ff8521f7 | ||
|
|
3794ddfd3a | ||
|
|
568084e143 | ||
|
|
64d979d5f9 | ||
|
|
0ef76f3e64 | ||
|
|
b8725ab13a | ||
|
|
6bf47f69eb | ||
|
|
0192c2bffa | ||
|
|
91aa815d88 | ||
|
|
53a493c233 | ||
|
|
dcdb9361e8 | ||
|
|
89531d1703 | ||
|
|
8311e842bb | ||
|
|
a492223369 | ||
|
|
cd9e336165 | ||
|
|
5b2ecac1f6 | ||
|
|
d2f7864266 | ||
|
|
c9432aa0d1 | ||
|
|
eeb2bae9f3 | ||
|
|
659ef0b6b9 | ||
|
|
9b83ac29b7 | ||
|
|
722b81623b | ||
|
|
648dd5adb2 | ||
|
|
75e7bb3255 | ||
|
|
10fc4d27ad | ||
|
|
ace422c0d5 | ||
|
|
b6682eed2a | ||
|
|
8080b8b56a | ||
|
|
0e8d04e464 | ||
|
|
fc76146085 | ||
|
|
4142413002 | ||
|
|
49e4855982 | ||
|
|
65cad2025a | ||
|
|
64327b3d25 | ||
|
|
54289368b1 | ||
|
|
297c226fab | ||
|
|
7f5b5f487e | ||
|
|
f2a6828015 | ||
|
|
45936805e9 | ||
|
|
0fae0b21ad | ||
|
|
9bd741b1d9 | ||
|
|
c15393981d | ||
|
|
9495d7d2ea | ||
|
|
788a95b260 | ||
|
|
4c697d0008 | ||
|
|
463e66081c | ||
|
|
a7e501570c | ||
|
|
4a76cb083a | ||
|
|
6a767ce5c4 | ||
|
|
f2a7cddbb0 | ||
|
|
f85f3a4eb5 | ||
|
|
0fcd132df4 | ||
|
|
0990cfe072 | ||
|
|
1312dd45e6 | ||
|
|
d92bf14b50 | ||
|
|
de8462e81e | ||
|
|
15bd58c746 | ||
|
|
26da18810d | ||
|
|
f80c99576b | ||
|
|
1e5a55917c | ||
|
|
375e43c191 | ||
|
|
feab4d2a51 | ||
|
|
73f0cdde7d | ||
|
|
810c2cbde4 | ||
|
|
a9ff9c3bc7 | ||
|
|
69faa1d493 | ||
|
|
f16afe20be | ||
|
|
a29b29300e | ||
|
|
56a0d89b88 | ||
|
|
d0cba30543 | ||
|
|
6d595dcdb6 | ||
|
|
2e49ef38c9 | ||
|
|
8ec5dd70e0 | ||
|
|
89363bfbff | ||
|
|
e45a03729b | ||
|
|
395ef82ad4 | ||
|
|
02f1b11ad6 | ||
|
|
c53fb9266c | ||
|
|
3d1edffd18 | ||
|
|
24ea3199dd | ||
|
|
44116424b0 | ||
|
|
a8868b5f0f | ||
|
|
5172f032e7 | ||
|
|
f8d30bf528 | ||
|
|
1c3e0bdd6a | ||
|
|
453a2224cd | ||
|
|
1b25a71d9f | ||
|
|
ba27ff9c42 | ||
|
|
02878d6a58 | ||
|
|
a676d23a54 | ||
|
|
36f1f778c3 | ||
|
|
f83404421a | ||
|
|
cd8abe5c06 | ||
|
|
471a2397c0 | ||
|
|
bf541f0898 | ||
|
|
a582a3b0ed | ||
|
|
20df70c449 | ||
|
|
af13b6c9f8 | ||
|
|
06ee82af0b | ||
|
|
57b466d80f | ||
|
|
dbd4d152ce | ||
|
|
95f47409d8 | ||
|
|
ed64b72676 | ||
|
|
f9948055e4 | ||
|
|
6ad709f93a | ||
|
|
f9498cf5a2 | ||
|
|
f5333c529c | ||
|
|
6b2cc23cd3 | ||
|
|
34329d04f0 | ||
|
|
701c283e97 | ||
|
|
f9fd589af2 | ||
|
|
0d9602693b | ||
|
|
27be568eb5 | ||
|
|
8f06aea736 | ||
|
|
65191538e2 | ||
|
|
8b1acbe13b | ||
|
|
d49485c363 | ||
|
|
1de028840e | ||
|
|
3f9fb68c5f | ||
|
|
33ef15511f | ||
|
|
139c384e97 | ||
|
|
ccb27dbdb9 | ||
|
|
6eb21ffd24 | ||
|
|
b9b3c8f78a | ||
|
|
4f4215577a | ||
|
|
ac0f56325b | ||
|
|
fd8df2a035 | ||
|
|
b4decb8f91 | ||
|
|
836c1e002e | ||
|
|
701701e13c | ||
|
|
6accf8420f | ||
|
|
ea5c3212d6 | ||
|
|
65705a8a1d | ||
|
|
8310ad9f24 | ||
|
|
1a5c2c4d3a | ||
|
|
cd689afaaa | ||
|
|
00e0eea60e | ||
|
|
d89ba767f1 | ||
|
|
c4acc95b06 | ||
|
|
ed9f792d21 | ||
|
|
31c008afa5 | ||
|
|
6e2c0bac43 | ||
|
|
a66920c51e | ||
|
|
1c9e2b546b | ||
|
|
9363004252 | ||
|
|
2297ff72f5 | ||
|
|
fbaa39a300 | ||
|
|
bd2c15c6bb | ||
|
|
a4d8f2db58 | ||
|
|
2a43255802 | ||
|
|
f3232b2d5c | ||
|
|
c7cb7d1ac4 | ||
|
|
bd5a23ff0d | ||
|
|
dd0c1eb74d | ||
|
|
9950ef16ce | ||
|
|
fed2c034ee | ||
|
|
4944e3c257 | ||
|
|
b865fa33f6 | ||
|
|
bb44ed15da | ||
|
|
93919421ee | ||
|
|
8d1831129b | ||
|
|
5e6e2c037e | ||
|
|
ddfceddc57 | ||
|
|
5b19ffd524 | ||
|
|
7422001ed5 | ||
|
|
3b0ef48cdf | ||
|
|
d13875e3e7 | ||
|
|
6ebc7fab47 | ||
|
|
9d931c1d13 | ||
|
|
f86b6ac3de | ||
|
|
ce1124284e | ||
|
|
233f19d23d | ||
|
|
89ef5fe33d | ||
|
|
7cd4345264 | ||
|
|
bbaa5dafb9 | ||
|
|
cee1b39640 | ||
|
|
e388410e34 | ||
|
|
c2d649d3f4 | ||
|
|
d9dec44c8f | ||
|
|
c94a3a98eb | ||
|
|
ec9b8359df | ||
|
|
3db89e3dee | ||
|
|
7f8dc74146 | ||
|
|
83cea33727 | ||
|
|
ada18a0413 | ||
|
|
96d85dcacd | ||
|
|
64ba033602 | ||
|
|
26807e80db | ||
|
|
e4f00285b3 | ||
|
|
d443e51d30 | ||
|
|
b4352f5f25 | ||
|
|
2c1b4a2547 | ||
|
|
b54685bed7 | ||
|
|
9032d5ac11 | ||
|
|
942747a0be | ||
|
|
fdd6463ac0 | ||
|
|
e9afc4ec0f | ||
|
|
dff811f615 | ||
|
|
d1e382ddf7 | ||
|
|
ccd460cf70 | ||
|
|
7b1ba912be | ||
|
|
f3864c9100 | ||
|
|
1082889b64 | ||
|
|
36839fdcb9 | ||
|
|
ebe54d47d8 | ||
|
|
7219823847 | ||
|
|
336fc7a64a | ||
|
|
84f62f8025 | ||
|
|
6bd6dfec49 | ||
|
|
a6e27f1312 | ||
|
|
c913df837a | ||
|
|
42bd9811e3 | ||
|
|
7fbf13fd9d | ||
|
|
fb086b5ad5 | ||
|
|
d4f51979d4 | ||
|
|
aeb54f59f1 | ||
|
|
5ca3b24527 | ||
|
|
88df2878cb | ||
|
|
f601bbc499 | ||
|
|
24e9ae6440 | ||
|
|
8c3f11622c | ||
|
|
da25ed431f | ||
|
|
ecc234a96a | ||
|
|
29851537eb | ||
|
|
5f2d7b32ae | ||
|
|
de00d49d7b | ||
|
|
bafa0f50fc | ||
|
|
80c75f9aeb | ||
|
|
407eba194d | ||
|
|
2eac3e6555 | ||
|
|
61a308cbc6 | ||
|
|
49b9a6f53d | ||
|
|
db1cc0d0e1 | ||
|
|
f89bd5b972 | ||
|
|
d94a8cb58c | ||
|
|
faf79e85fd | ||
|
|
0d72e88c6a | ||
|
|
786ec7fff1 | ||
|
|
8df497e53d | ||
|
|
1dd7a6ebac | ||
|
|
2c12e9f64b | ||
|
|
9a77135d30 | ||
|
|
f3b3b9b3f0 | ||
|
|
55c4d4d03d | ||
|
|
061d341d8b | ||
|
|
1c29b8b260 | ||
|
|
79bfe9c866 | ||
|
|
3e6d38656d | ||
|
|
5697486cea | ||
|
|
df0da605e4 | ||
|
|
fa9aaf0423 | ||
|
|
89de288fec | ||
|
|
940003552b | ||
|
|
0660296b51 | ||
|
|
affeb0a169 | ||
|
|
2504653426 | ||
|
|
b6a9ad67d3 | ||
|
|
3d4741eac2 | ||
|
|
5611f57e9e | ||
|
|
44027f5bc0 | ||
|
|
41b25a78e9 | ||
|
|
e263f94765 | ||
|
|
80c7754e48 | ||
|
|
165340324c | ||
|
|
735dddd604 | ||
|
|
3e3bd32705 | ||
|
|
f83025a9ff | ||
|
|
ba6a7b58aa | ||
|
|
fb8808ea59 | ||
|
|
73f241e9c3 | ||
|
|
317b1b6ac5 | ||
|
|
cabe629f17 | ||
|
|
9f6521b987 | ||
|
|
6daffe5b13 | ||
|
|
4c7f93d1ef | ||
|
|
f81bbb93a5 | ||
|
|
b0058e94ce | ||
|
|
de069f704a | ||
|
|
bcb8493cd0 | ||
|
|
e55b1740db | ||
|
|
df673b2a4e | ||
|
|
a4753769d2 | ||
|
|
be75a87e88 | ||
|
|
0ac3ae1cb1 | ||
|
|
a5e1e95534 | ||
|
|
7f6ab0b854 | ||
|
|
8420ecd380 | ||
|
|
192fd09a00 | ||
|
|
d9a59c6d1e | ||
|
|
69c3c3162c | ||
|
|
61ba832dfd | ||
|
|
f332bba468 | ||
|
|
ba8cca6ba5 | ||
|
|
7a098952c8 | ||
|
|
347b74a55d | ||
|
|
d5b0adeeed | ||
|
|
d8c4d36d4b | ||
|
|
aac32c5bff | ||
|
|
ff86e55622 | ||
|
|
894c4cb139 | ||
|
|
2792016383 | ||
|
|
46215871aa | ||
|
|
cfc06be975 | ||
|
|
527589ac04 | ||
|
|
43845cda5c | ||
|
|
b74b8a8a5a | ||
|
|
226c6d8432 | ||
|
|
9f79258dec | ||
|
|
13bcc99095 | ||
|
|
fef9b93a05 | ||
|
|
ccac3437cf | ||
|
|
e849a31668 | ||
|
|
88de5412f8 | ||
|
|
d13c48d31d | ||
|
|
4c807866a3 | ||
|
|
d7a4a95c05 | ||
|
|
b35422ff9f | ||
|
|
b6be5d71bb | ||
|
|
4c5eddcf6d | ||
|
|
51f0b75a64 | ||
|
|
fe4648cd9e | ||
|
|
03867ada49 | ||
|
|
7959188c06 | ||
|
|
11eaa0ca50 | ||
|
|
dc6dba416a | ||
|
|
c8e7cc773a | ||
|
|
efe43329a1 | ||
|
|
3f97c17af2 | ||
|
|
91493e8769 | ||
|
|
5f36cb88ab | ||
|
|
a0ef635c92 | ||
|
|
d68904fec0 | ||
|
|
b679581cf2 | ||
|
|
f7e6fa026d | ||
|
|
96f16f1f2e | ||
|
|
ad8fa8722f | ||
|
|
e477f09cd5 | ||
|
|
be5eb9ef70 | ||
|
|
b952642570 | ||
|
|
47cc74a351 | ||
|
|
cff572a104 | ||
|
|
48e16e64c2 | ||
|
|
46172836f1 | ||
|
|
eacb72a05b | ||
|
|
c1fefaab92 | ||
|
|
f2f86457ee | ||
|
|
c251b5831b | ||
|
|
8c589d3000 | ||
|
|
ddc599f6b7 | ||
|
|
6822c3a04b | ||
|
|
aa0c70bd44 | ||
|
|
94f649b345 | ||
|
|
5e07e9dceb | ||
|
|
91a8a8be34 | ||
|
|
99e1890795 | ||
|
|
5583befbba | ||
|
|
c637055859 | ||
|
|
c3acfb8781 | ||
|
|
fd073a7043 | ||
|
|
bed00fbd41 | ||
|
|
501b79fdce | ||
|
|
ed6af8f560 | ||
|
|
32495736d4 | ||
|
|
bff48b0a64 | ||
|
|
f6228240ba | ||
|
|
215c8a7ff4 | ||
|
|
0f217bd753 | ||
|
|
7e920f4bae | ||
|
|
cde3d878b1 | ||
|
|
621a6ea42d | ||
|
|
cfc3615e64 | ||
|
|
2f8d0d90cd | ||
|
|
c827953ca5 | ||
|
|
79dd263fb1 | ||
|
|
1ca05a029a | ||
|
|
4c205eb09d | ||
|
|
ee92f6639a | ||
|
|
7721f16f9f | ||
|
|
854222b8cc | ||
|
|
0bc86541c6 | ||
|
|
c45111333d | ||
|
|
ba6fedc430 | ||
|
|
caa34fa8bb | ||
|
|
fa7c0ab58e | ||
|
|
2543eb6861 | ||
|
|
c4cf3363ee | ||
|
|
98d4efd509 | ||
|
|
a99e65ee48 | ||
|
|
3fd4d24c93 | ||
|
|
55a564a5a8 | ||
|
|
04f6490eeb | ||
|
|
b5026789d6 | ||
|
|
705e570cf5 | ||
|
|
10a41fb0d1 | ||
|
|
2a591646c3 | ||
|
|
e55898b414 | ||
|
|
2df476406d | ||
|
|
4a05b35f2b | ||
|
|
3ad8e56c25 | ||
|
|
c1704758fd | ||
|
|
fe580f2b2b | ||
|
|
d82e482acc | ||
|
|
9d6142dc79 | ||
|
|
2dec83735b | ||
|
|
9f4204b815 | ||
|
|
40f2c972d7 | ||
|
|
eee2385d0d | ||
|
|
492c652157 | ||
|
|
af8e060dc6 | ||
|
|
6a19b9058f | ||
|
|
bcbd21b922 | ||
|
|
aac76d68b0 | ||
|
|
25bb942e04 | ||
|
|
8ac71f21d1 | ||
|
|
287559f028 | ||
|
|
7fbb93cf41 | ||
|
|
7b76e93631 | ||
|
|
896c451c3e | ||
|
|
0874386001 | ||
|
|
ce6808b3c4 | ||
|
|
40fa1c5acb | ||
|
|
7307990f6f | ||
|
|
5104da500e | ||
|
|
3a8c46bbed | ||
|
|
fc5312549d | ||
|
|
c14e8797e2 | ||
|
|
0f26940018 | ||
|
|
26227e2f3b | ||
|
|
dcf78fab06 | ||
|
|
0813592a6d | ||
|
|
abdb27af3f | ||
|
|
b421be3315 | ||
|
|
b74fbee069 | ||
|
|
dab9357b40 | ||
|
|
ccd89604c0 | ||
|
|
cd135b7171 | ||
|
|
7e16d550b0 | ||
|
|
404079ef4e | ||
|
|
d13f78f046 | ||
|
|
60040c3914 | ||
|
|
e408fbd8d6 | ||
|
|
6931ecd468 | ||
|
|
ed5d30ea5b | ||
|
|
ead7613579 | ||
|
|
ca0613ec17 | ||
|
|
727208ff84 | ||
|
|
53c3de2af5 | ||
|
|
3f50d57ed1 | ||
|
|
55f9d0f875 | ||
|
|
37cb9b0fe8 | ||
|
|
a3b5f79094 | ||
|
|
1b1ffa7109 | ||
|
|
073578243c | ||
|
|
e9c40692a6 | ||
|
|
995a26b944 | ||
|
|
96ba7d0656 | ||
|
|
6852319e4d | ||
|
|
8bc1eaebc0 | ||
|
|
eeefaa6374 | ||
|
|
fb6aec0afe | ||
|
|
b8a48314c1 | ||
|
|
1eb52d8a35 | ||
|
|
8fee195577 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
|
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
|
||||||
# Latest version available on this commit is 1.71.1
|
# Latest version available on this commit is 1.71.1
|
||||||
# Commit date is Aug 3, 2023
|
# Commit date is Aug 3, 2023
|
||||||
uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
|
uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ jobs:
|
|||||||
spec: |
|
spec: |
|
||||||
cypress/e2e/mainnet/*.spec.ts
|
cypress/e2e/mainnet/*.spec.ts
|
||||||
cypress/e2e/signet/*.spec.ts
|
cypress/e2e/signet/*.spec.ts
|
||||||
cypress/e2e/testnet/*.spec.ts
|
cypress/e2e/testnet4/*.spec.ts
|
||||||
- module: "liquid"
|
- module: "liquid"
|
||||||
spec: |
|
spec: |
|
||||||
cypress/e2e/liquid/liquid.spec.ts
|
cypress/e2e/liquid/liquid.spec.ts
|
||||||
|
|||||||
12
LICENSE
12
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
The Mempool Open Source Project®
|
The Mempool Open Source Project®
|
||||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it under
|
This program is free software; you can redistribute it and/or modify it under
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
@@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
|
|||||||
|
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||||
|
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||||
|
the United States, and/or other countries.
|
||||||
|
|
||||||
See our full Trademark Policy and Guidelines for more details, published on
|
See our full Trademark Policy and Guidelines for more details, published on
|
||||||
<https://mempool.space/trademark-policy>.
|
<https://mempool.space/trademark-policy>.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@typescript-eslint/no-this-alias": 1,
|
"@typescript-eslint/no-this-alias": 1,
|
||||||
"@typescript-eslint/no-var-requires": 1,
|
"@typescript-eslint/no-var-requires": 1,
|
||||||
"@typescript-eslint/explicit-function-return-type": 1,
|
"@typescript-eslint/explicit-function-return-type": 1,
|
||||||
|
"@typescript-eslint/no-unused-vars": 1,
|
||||||
"no-console": 1,
|
"no-console": 1,
|
||||||
"no-constant-condition": 1,
|
"no-constant-condition": 1,
|
||||||
"no-dupe-else-if": 1,
|
"no-dupe-else-if": 1,
|
||||||
@@ -32,6 +33,8 @@
|
|||||||
"prefer-rest-params": 1,
|
"prefer-rest-params": 1,
|
||||||
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
||||||
"semi": 1,
|
"semi": 1,
|
||||||
"eqeqeq": 1
|
"curly": [1, "all"],
|
||||||
|
"eqeqeq": 1,
|
||||||
|
"no-trailing-spaces": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
|||||||
|
|
||||||
#### Build
|
#### Build
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer_
|
||||||
|
|
||||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ In particular, make sure:
|
|||||||
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
|
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
|
||||||
- the correct `BACKEND` is specified in `MEMPOOL`:
|
- the correct `BACKEND` is specified in `MEMPOOL`:
|
||||||
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
|
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
|
||||||
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
|
- "esplora" if you're using [mempool/electrs](https://github.com/mempool/electrs)
|
||||||
- "none" if you're not using any Electrum Server
|
- "none" if you're not using any Electrum Server
|
||||||
|
|
||||||
### 6. Run Mempool Backend
|
### 6. Run Mempool Backend
|
||||||
@@ -181,7 +181,7 @@ Create a new wallet, if needed:
|
|||||||
bitcoin-cli -regtest createwallet test
|
bitcoin-cli -regtest createwallet test
|
||||||
```
|
```
|
||||||
|
|
||||||
Load wallet (this command may take a while if you have lot of UTXOs):
|
Load wallet (this command may take a while if you have a lot of UTXOs):
|
||||||
```
|
```
|
||||||
bitcoin-cli -regtest loadwallet test
|
bitcoin-cli -regtest loadwallet test
|
||||||
```
|
```
|
||||||
@@ -229,13 +229,13 @@ Generate block at regular interval (every 10 seconds in this example):
|
|||||||
|
|
||||||
### Mining pools update
|
### Mining pools update
|
||||||
|
|
||||||
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).
|
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` is set to `false`).
|
||||||
|
|
||||||
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
|
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
|
||||||
|
|
||||||
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
|
You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` to `true` in your `mempool-config.json`.
|
||||||
|
|
||||||
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
|
When a `coinbase tag` or `coinbase address` change is detected, pool assignments for all relevant blocks (tagged to that pool or the `unknown` mining pool, starting from height 130635) are updated using the new criteria.
|
||||||
|
|
||||||
### Re-index tables
|
### Re-index tables
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"EXTERNAL_RETRY_INTERVAL": 0,
|
"EXTERNAL_RETRY_INTERVAL": 0,
|
||||||
"USER_AGENT": "mempool",
|
"USER_AGENT": "mempool",
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
"ALLOW_UNREACHABLE": true,
|
"ALLOW_UNREACHABLE": true,
|
||||||
"PRICE_UPDATES_PER_HOUR": 1,
|
"PRICE_UPDATES_PER_HOUR": 1,
|
||||||
"MAX_TRACKED_ADDRESSES": 100
|
"MAX_TRACKED_ADDRESSES": 100,
|
||||||
|
"UNIX_SOCKET_PATH": ""
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@@ -58,7 +59,8 @@
|
|||||||
"RETRY_UNIX_SOCKET_AFTER": 30000,
|
"RETRY_UNIX_SOCKET_AFTER": 30000,
|
||||||
"REQUEST_TIMEOUT": 10000,
|
"REQUEST_TIMEOUT": 10000,
|
||||||
"FALLBACK_TIMEOUT": 5000,
|
"FALLBACK_TIMEOUT": 5000,
|
||||||
"FALLBACK": []
|
"FALLBACK": [],
|
||||||
|
"MAX_BEHIND_TIP": 2
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@@ -138,6 +140,8 @@
|
|||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"AUDIT_START_HEIGHT": 774000,
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"STATISTICS": false,
|
||||||
|
"STATISTICS_START_TIME": 1481932800,
|
||||||
"SERVERS": [
|
"SERVERS": [
|
||||||
"list",
|
"list",
|
||||||
"of",
|
"of",
|
||||||
@@ -151,6 +155,7 @@
|
|||||||
},
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
|
"PAID": false,
|
||||||
"API_KEY": "your-api-key-from-freecurrencyapi.com"
|
"API_KEY": "your-api-key-from-freecurrencyapi.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
975
backend/package-lock.json
generated
975
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.0.0-dev",
|
"version": "3.1.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@@ -39,29 +39,29 @@
|
|||||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.25.2",
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "~1.6.1",
|
"axios": "1.7.2",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.19.2",
|
"express": "~4.21.0",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.9.1",
|
"mysql2": "~3.11.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
"redis": "^4.6.6",
|
"redis": "^4.7.0",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.9.3",
|
"typescript": "~4.9.3",
|
||||||
"ws": "~8.13.0"
|
"ws": "~8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/ws": "~8.5.5",
|
"@types/ws": "~8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
"@typescript-eslint/parser": "^5.55.0",
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||||
"GOGGLES_INDEXING": false,
|
"GOGGLES_INDEXING": false,
|
||||||
"HTTP_PORT": 1,
|
"HTTP_PORT": 1,
|
||||||
|
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
|
||||||
"SPAWN_CLUSTER_PROCS": 2,
|
"SPAWN_CLUSTER_PROCS": 2,
|
||||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POLL_RATE_MS": 3,
|
"POLL_RATE_MS": 3,
|
||||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||||
"CACHE_ENABLED": true,
|
"CACHE_ENABLED": true,
|
||||||
@@ -59,7 +60,8 @@
|
|||||||
"RETRY_UNIX_SOCKET_AFTER": 888,
|
"RETRY_UNIX_SOCKET_AFTER": 888,
|
||||||
"REQUEST_TIMEOUT": 10000,
|
"REQUEST_TIMEOUT": 10000,
|
||||||
"FALLBACK_TIMEOUT": 5000,
|
"FALLBACK_TIMEOUT": 5000,
|
||||||
"FALLBACK": []
|
"FALLBACK": [],
|
||||||
|
"MAX_BEHIND_TIP": 2
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||||
@@ -130,6 +132,8 @@
|
|||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"AUDIT_START_HEIGHT": 774000,
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"STATISTICS": false,
|
||||||
|
"STATISTICS_START_TIME": 1481932800,
|
||||||
"SERVERS": []
|
"SERVERS": []
|
||||||
},
|
},
|
||||||
"MEMPOOL_SERVICES": {
|
"MEMPOOL_SERVICES": {
|
||||||
@@ -143,6 +147,7 @@
|
|||||||
},
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
|
"PAID": false,
|
||||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
|
||||||
|
|
||||||
const randomTransactions = require('./test-data/transactions-random.json');
|
const randomTransactions = require('./test-data/transactions-random.json');
|
||||||
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||||
const rbfTransactions = require('./test-data/transactions-rbfs.json');
|
const rbfTransactions = require('./test-data/transactions-rbfs.json');
|
||||||
|
const nonStandardTransactions = require('./test-data/non-standard-txs.json');
|
||||||
|
|
||||||
describe('Mempool Utils', () => {
|
describe('Common', () => {
|
||||||
test('should detect RBF transactions with fast method', () => {
|
describe('RBF', () => {
|
||||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
test('should detect RBF transactions with fast method', () => {
|
||||||
expect(Object.values(result).length).toEqual(2);
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
expect(Object.values(result).length).toEqual(2);
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect RBF transactions with scalable method', () => {
|
||||||
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||||
|
expect(Object.values(result).length).toEqual(2);
|
||||||
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only('should detect RBF transactions with scalable method', () => {
|
describe('Mempool Goggles', () => {
|
||||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
test('should detect nonstandard transactions', () => {
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
nonStandardTransactions.forEach((tx) => {
|
||||||
expect(Object.values(result).length).toEqual(2);
|
expect(Common.isNonStandard(tx)).toEqual(true);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
});
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
});
|
||||||
|
|
||||||
|
test('should not misclassify as nonstandard transactions', () => {
|
||||||
|
randomTransactions.forEach((tx) => {
|
||||||
|
expect(Common.isNonStandard(tx)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
52
backend/src/__tests__/api/test-data/non-standard-txs.json
Normal file
52
backend/src/__tests__/api/test-data/non-standard-txs.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367",
|
||||||
|
"version": 1,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019",
|
||||||
|
"vout": 4217,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33",
|
||||||
|
"value": 106
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01",
|
||||||
|
"02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"scriptpubkey": "6a023a29",
|
||||||
|
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29",
|
||||||
|
"scriptpubkey_type": "op_return",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "6a036d7648",
|
||||||
|
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648",
|
||||||
|
"scriptpubkey_type": "op_return",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 186,
|
||||||
|
"weight": 420,
|
||||||
|
"sigops": 1,
|
||||||
|
"fee": 106,
|
||||||
|
"status": {
|
||||||
|
"confirmed": true,
|
||||||
|
"block_height": 836361,
|
||||||
|
"block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b",
|
||||||
|
"block_time": 1711448028
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -273,5 +273,328 @@
|
|||||||
},
|
},
|
||||||
"bestDescendant": null,
|
"bestDescendant": null,
|
||||||
"cpfpChecked": true
|
"cpfpChecked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "20b984492b5264162a4c92c9a34bc7fa08b67d669de7b4c5982ad3cb28aaecf6",
|
||||||
|
"version": 2,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "3adda6afd547193793c248e667c2b7dbf26d705003de65e3a25e5be698286aef",
|
||||||
|
"vout": 2,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
|
||||||
|
"value": 27619
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"304402205d7f1e0d928982645c2bcc4c730c4545c382d6520c2a14eebc71594702cd06b302200511d452ce51c79017536f50acb115eefe7c04506ad12b9307d2b5d56b999beb01",
|
||||||
|
"03716cb4f0430fe69c596a12c6680c55803150645989b406772838d548cde7cca5"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"scriptpubkey": "6a5d0614c0a2331441",
|
||||||
|
"scriptpubkey_asm": "OP_RETURN OP_PUSHNUM_13 OP_PUSHBYTES_6 14c0a2331441",
|
||||||
|
"scriptpubkey_type": "op_return",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "5114d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_20 d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
|
||||||
|
"scriptpubkey_type": "unknown",
|
||||||
|
"scriptpubkey_address": "bc1p6uwxc048hflxaerh5zlastpqc7ye0zpvq7gq2a",
|
||||||
|
"value": 546
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
|
||||||
|
"value": 23073
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 240,
|
||||||
|
"weight": 633,
|
||||||
|
"sigops": 1,
|
||||||
|
"fee": 4000,
|
||||||
|
"status": {
|
||||||
|
"confirmed": true,
|
||||||
|
"block_height": 848136,
|
||||||
|
"block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d",
|
||||||
|
"block_time": 1718517071
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "b10c0000004da5a9d1d9b4ae32e09f0b3e62d21a5cce5428d4ad714fb444eb5d",
|
||||||
|
"version": 1,
|
||||||
|
"locktime": 1231006505,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "d46a24962c1d7bd6e87d80570c6a53413eaf30d7fde7f52347f13645ae53969b",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "41049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cfac",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHBYTES_65 049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cf OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pk",
|
||||||
|
"value": 6102
|
||||||
|
},
|
||||||
|
"scriptsig": "473044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_71 3044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 20090103
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "76a914bbb1f7d0f7e15ac088af9bafe25aaac1a59832d088ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 bbb1f7d0f7e15ac088af9bafe25aaac1a59832d0 OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1J7SZJry7CX4zWdH3P8E8UJjZrhcLEjJ39",
|
||||||
|
"value": 1913
|
||||||
|
},
|
||||||
|
"scriptsig": "46304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad8510221028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_70 304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad85102 OP_PUSHBYTES_33 028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 20081031
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
|
||||||
|
"vout": 1,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "52210304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f2102b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f53ae",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 0304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f OP_PUSHBYTES_33 02b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_PUSHNUM_3 OP_CHECKMULTISIG",
|
||||||
|
"scriptpubkey_type": "multisig",
|
||||||
|
"value": 1971
|
||||||
|
},
|
||||||
|
"scriptsig": "00453042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e2033b303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481",
|
||||||
|
"scriptsig_asm": "OP_0 OP_PUSHBYTES_69 3042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e203 OP_PUSHBYTES_59 303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 19750504
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "45e1cb33599acb071810ccc801b71bd7610865f5b899492946ab1bfbcb61cad6",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "a91419f0b86f61606c6eb51b217698ca7e8bff1e398b87",
|
||||||
|
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 19f0b86f61606c6eb51b217698ca7e8bff1e398b OP_EQUAL",
|
||||||
|
"scriptpubkey_type": "p2sh",
|
||||||
|
"scriptpubkey_address": "344BBtYkhaCXgA7oYSXASUfh4bFieiponG",
|
||||||
|
"value": 2140
|
||||||
|
},
|
||||||
|
"scriptsig": "00443041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea013a303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef8239303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba834ced532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae",
|
||||||
|
"scriptsig_asm": "OP_0 OP_PUSHBYTES_68 3041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea01 OP_PUSHBYTES_58 303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef82 OP_PUSHBYTES_57 303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba83 OP_PUSHDATA1 532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 16,
|
||||||
|
"inner_redeemscript_asm": "OP_PUSHNUM_3 OP_PUSHBYTES_33 03e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc OP_PUSHBYTES_33 03cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd4 OP_PUSHBYTES_33 027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906 OP_PUSHBYTES_65 0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_PUSHBYTES_65 04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_PUSHNUM_5 OP_CHECKMULTISIG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
|
||||||
|
"vout": 2,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "a9143b13a1f71c20c799d86bb624b3898c826d6c82da87",
|
||||||
|
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 3b13a1f71c20c799d86bb624b3898c826d6c82da OP_EQUAL",
|
||||||
|
"scriptpubkey_type": "p2sh",
|
||||||
|
"scriptpubkey_address": "375PJxsKRtAq4WoS6u82jvgZW94R8Wx3iH",
|
||||||
|
"value": 5139
|
||||||
|
},
|
||||||
|
"scriptsig": "1600149b27f072e4b972927c445d1946162a550b0914d8",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_22 00149b27f072e4b972927c445d1946162a550b0914d8",
|
||||||
|
"witness": [
|
||||||
|
"3040021c23902a01d4c5cff2c33c8bdb778a5aadea78a9a0d6d4db60aaa0fba1022069237d9dbf2db8cff9c260ba71250493682d01a746f4a45c5c7ea386e56d2bc902",
|
||||||
|
"0240187acd3e2fd3d8e1acffefa85907b6550730c24f78dfd3301c829fc4daf3cc"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 141,
|
||||||
|
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_20 9b27f072e4b972927c445d1946162a550b0914d8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
|
||||||
|
"vout": 3,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "a914a3c0698f2300c7b2e8107d4c9c988e642110039087",
|
||||||
|
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3c0698f2300c7b2e8107d4c9c988e6421100390 OP_EQUAL",
|
||||||
|
"scriptpubkey_type": "p2sh",
|
||||||
|
"scriptpubkey_address": "3GcrZrbUuvE4UtUdSbKTXcRnTqmfMdyMAC",
|
||||||
|
"value": 3220
|
||||||
|
},
|
||||||
|
"scriptsig": "220020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_34 0020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
|
||||||
|
"witness": [
|
||||||
|
"303f021c65aee6696e80be6e14545cfd64b44f17b0514c150eefdb090c0f0bd9021f3fef4aa95c252a225622aba99e4d5af5a6fe40d177acd593e64cf2f8557ccc03",
|
||||||
|
"03b55c6f0749e0f3e2caeca05f68e3699f1b3c62a550730f704985a6a9aae437a1",
|
||||||
|
"76a914db865fd920959506111079995f1e4017b489bfe38763ac6721024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c7c820120876475527c2103443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca52ae67a91446c3747322b220fdb925c9802f0e949c1feab99988ac6868"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 3735928559,
|
||||||
|
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
|
||||||
|
"inner_witnessscript_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 db865fd920959506111079995f1e4017b489bfe3 OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 03443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 46c3747322b220fdb925c9802f0e949c1feab999 OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF OP_ENDIF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
|
||||||
|
"vout": 4,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "0014c0ca6e754e65d3ba59112d7abc33e500c00ecfa7",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c0ca6e754e65d3ba59112d7abc33e500c00ecfa7",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qcr9xua2wvhfm5kg394atcvl9qrqqana8rrmy8h",
|
||||||
|
"value": 17144
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"303e021c11f60486afd0f5d6573603fb2076ef2f676455b92ada257d2f25558a021e317719c946f951d49bf4df4285a618629cd9e554fcbf787c319a0c4dd22601",
|
||||||
|
"032467f24cc31664f0cf34ff8d5cbb590888ddc1dcfec724a32ae3dd5338b8508e"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 21000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "637db3928a8fb1b22b81f92dc738ee7637e5b172d650363d0b327429578bd001",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "0020a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357",
|
||||||
|
"scriptpubkey_type": "v0_p2wsh",
|
||||||
|
"scriptpubkey_address": "bc1q49fs59nletdxwtq59mnrdhx3w9uku6003cm658mh7dw93mwh5dts2w2kht",
|
||||||
|
"value": 8149
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"303d021c32f9454db85cb1a4ca63a9883d4347c5e13f3654e884ae44e9efa3c8021d62f07fe452c06b084bc3e09afd3aac4039136549a465533bc1ca66967902",
|
||||||
|
"01",
|
||||||
|
"632102fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd67012ab27521034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f68ac"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 4190024921,
|
||||||
|
"inner_witnessscript_asm": "OP_IF OP_PUSHBYTES_33 02fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd OP_ELSE OP_PUSHBYTES_1 2a OP_CSV OP_DROP OP_PUSHBYTES_33 034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f OP_ENDIF OP_CHECKSIG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "0020db02df125062ebae5bacd189ebff22577b2817c1872be79a0d3ba3982c41",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "512071212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 71212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849",
|
||||||
|
"scriptpubkey_type": "v1_p2tr",
|
||||||
|
"scriptpubkey_address": "bc1pwysjmmg07nymrvx9qhvqzfmjutd7nz3u4ecksdmmj58mdwrx4pysq6m68g",
|
||||||
|
"value": 9001
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"d822f203827852998cad370232e8c57294540a5da51107fa26cf466bdd2b8b0b3d161999cc80aed8de7386a2bd5d5313aea159a231cc26fa53aaa702b7fa21ed"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 341
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "795741ecf9c431b14b1c8d2dd017d3978fd4f6452e91edf416f31ef9971206b4",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "512089ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 89ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a",
|
||||||
|
"scriptpubkey_type": "v1_p2tr",
|
||||||
|
"scriptpubkey_address": "bc1p3xkpyzjfpmhg3k643qgjl90cszfjsnypfuru8tv58fl6a7azyudqkcu66k",
|
||||||
|
"value": 19953
|
||||||
|
},
|
||||||
|
"scriptsig": "",
|
||||||
|
"scriptsig_asm": "",
|
||||||
|
"witness": [
|
||||||
|
"fe6eb715dceffefc067fdc787d250a9a9116682d216f6356ea38fc1f112bd74995faa90315e81981d2c2260b7eaca3c41a16b280362980f0d8faf4c05ebb82c5",
|
||||||
|
"e34ad0ad33885a473831f8ba8d9339123cb19d0e642e156d8e0d6e2ab2691aedb30e55a35637a806927225e1aa72223d41e59f92c6579b819e7d331a7ada9d2e01",
|
||||||
|
"2a4861fb4cb951c791bf6c93859ef65abccd90034f91b9b77abb918e13b6fce75d5fa3e2d2f6eeeae105315178c2cb9db2ef238fe89b282f691c06db43bc71ca02",
|
||||||
|
"fc97bb2be673c3bf388aaf58178ef14d354caf83c92aca8ef1831d619b8511e928f4f5fdea3962067b11e7cecfe094cd0f66a4ea9af9ec836d70d18f2b37df0281",
|
||||||
|
"a5781a0adaa80ab7f7f164172dd1a1cb127e523daa0d6949aba074a15c589f12dfb8183182afec9230cb7947b7422a4abc1bb78173550d66274ea19f6c9dd92c82",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"205f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1ac205f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3ba205ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996ba20b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690ba20d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5ba20cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0ba20aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545ba559c",
|
||||||
|
"c0b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f5534a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e166f7cf9580f1c2dfb3c4d5d043cdbb128c640e3f20161245aa7372e9666168516a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48dd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb46829a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713d29c9c0e8e4d2a9790922af73f0b8d51f0bd4bb19940d9cf910ead8fbe85bc9bbb41a757f405890fb0f5856228e23b715702d714d59bf2b1feb70d8b2b4e3e089fdbcf0ef9d8d00f66e47917f67cc5d78aec1ac786e2abb8d2facb4e4790aad6cc455ae816e6cdafdb58d54e35d4f46d860047458eacf1c7405dc634631c570d8d31992805518fd62daa3bdd2a5c4fd2cd3054c9b3dca1d78055e9528cff6adc8f907925d2ebe48765103e6845c06f1f2bb77c6adc1cc002865865eb5cfd5c1cb10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d4133e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac9879903637777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8fd456524104a6674693c29946543f8a0befccce5a352bda55ec8559fc630f5f37393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef7373be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7eb5a2af5845a8d3796308ff9840e567b14cf6bb158ff26c999e6f9a1f5448f9aa"
|
||||||
|
],
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 342,
|
||||||
|
"inner_witnessscript_asm": "OP_PUSHBYTES_32 5f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1 OP_CHECKSIG OP_PUSHBYTES_32 5f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3 OP_CHECKSIGADD OP_PUSHBYTES_32 5ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996 OP_CHECKSIGADD OP_PUSHBYTES_32 b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690 OP_CHECKSIGADD OP_PUSHBYTES_32 d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5 OP_CHECKSIGADD OP_PUSHBYTES_32 cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0 OP_CHECKSIGADD OP_PUSHBYTES_32 aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545 OP_CHECKSIGADD OP_PUSHNUM_5 OP_NUMEQUAL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"scriptpubkey": "210261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32aac",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHBYTES_33 0261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32a OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pk",
|
||||||
|
"value": 576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "76a9140240539af6c68431e4ce9cc5ef464f12c1741b3c88ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 0240539af6c68431e4ce9cc5ef464f12c1741b3c OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1CuQsdrcgcmPvugo3NqEwh1kDcpeEnuFC",
|
||||||
|
"value": 546
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "5121028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae2851ae",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_33 028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae28 OP_PUSHNUM_1 OP_CHECKMULTISIG",
|
||||||
|
"scriptpubkey_type": "multisig",
|
||||||
|
"value": 582
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "a91449ed2c96e33b6134408af8484508bcc3248c8dbd87",
|
||||||
|
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 49ed2c96e33b6134408af8484508bcc3248c8dbd OP_EQUAL",
|
||||||
|
"scriptpubkey_type": "p2sh",
|
||||||
|
"scriptpubkey_address": "38RuNhSiZiftB6WVnStu5aUz6jXtCDXQZk",
|
||||||
|
"value": 540
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "0014c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qerj3ea5frs9zzqdwe65v6h8fhwl677a6s0hxhf",
|
||||||
|
"value": 294
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "0020c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a",
|
||||||
|
"scriptpubkey_type": "v0_p2wsh",
|
||||||
|
"scriptpubkey_address": "bc1qcjzmhwqvf038dem74safsw3ernytrgd479sfjk3kc00lrq5k8pdqczl83q",
|
||||||
|
"value": 330
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "5120a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9",
|
||||||
|
"scriptpubkey_type": "v1_p2tr",
|
||||||
|
"scriptpubkey_address": "bc1p57jzkf5f27sxe80y6unq780njt8y6mnmwsl44hp8g9ww9t7wkwusv7av76",
|
||||||
|
"value": 330
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "51024e73",
|
||||||
|
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_2 4e73",
|
||||||
|
"scriptpubkey_type": "unknown",
|
||||||
|
"scriptpubkey_address": "bc1pfeessrawgf",
|
||||||
|
"value": 240
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "6a224e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e005152535455565758595a5b5c5d5e5f60",
|
||||||
|
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_34 4e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e OP_0 OP_PUSHNUM_1 OP_PUSHNUM_2 OP_PUSHNUM_3 OP_PUSHNUM_4 OP_PUSHNUM_5 OP_PUSHNUM_6 OP_PUSHNUM_7 OP_PUSHNUM_8 OP_PUSHNUM_9 OP_PUSHNUM_10 OP_PUSHNUM_11 OP_PUSHNUM_12 OP_PUSHNUM_13 OP_PUSHNUM_14 OP_PUSHNUM_15 OP_PUSHNUM_16",
|
||||||
|
"scriptpubkey_type": "op_return",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 3500,
|
||||||
|
"weight": 8186,
|
||||||
|
"sigops": 115,
|
||||||
|
"fee": 71294,
|
||||||
|
"status": {
|
||||||
|
"confirmed": true,
|
||||||
|
"block_height": 850000,
|
||||||
|
"block_hash": "00000000000000000002a0b5db2a7f8d9087464c2586b546be7bce8eb53b8187",
|
||||||
|
"block_time": 1719689674
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -20,9 +20,10 @@ describe('Mempool Backend Config', () => {
|
|||||||
BLOCKS_SUMMARIES_INDEXING: false,
|
BLOCKS_SUMMARIES_INDEXING: false,
|
||||||
GOGGLES_INDEXING: false,
|
GOGGLES_INDEXING: false,
|
||||||
HTTP_PORT: 8999,
|
HTTP_PORT: 8999,
|
||||||
|
UNIX_SOCKET_PATH: '',
|
||||||
SPAWN_CLUSTER_PROCS: 0,
|
SPAWN_CLUSTER_PROCS: 0,
|
||||||
API_URL_PREFIX: '/api/v1/',
|
API_URL_PREFIX: '/api/v1/',
|
||||||
AUTOMATIC_BLOCK_REINDEXING: false,
|
AUTOMATIC_POOLS_UPDATE: false,
|
||||||
POLL_RATE_MS: 2000,
|
POLL_RATE_MS: 2000,
|
||||||
CACHE_DIR: './cache',
|
CACHE_DIR: './cache',
|
||||||
CACHE_ENABLED: true,
|
CACHE_ENABLED: true,
|
||||||
@@ -62,6 +63,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
REQUEST_TIMEOUT: 10000,
|
REQUEST_TIMEOUT: 10000,
|
||||||
FALLBACK_TIMEOUT: 5000,
|
FALLBACK_TIMEOUT: 5000,
|
||||||
FALLBACK: [],
|
FALLBACK: [],
|
||||||
|
MAX_BEHIND_TIP: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.CORE_RPC).toStrictEqual({
|
expect(config.CORE_RPC).toStrictEqual({
|
||||||
@@ -134,6 +136,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
ENABLED: false,
|
ENABLED: false,
|
||||||
AUDIT: false,
|
AUDIT: false,
|
||||||
AUDIT_START_HEIGHT: 774000,
|
AUDIT_START_HEIGHT: 774000,
|
||||||
|
STATISTICS: false,
|
||||||
|
STATISTICS_START_TIME: 1481932800,
|
||||||
SERVERS: []
|
SERVERS: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,6 +154,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
|
|
||||||
expect(config.FIAT_PRICE).toStrictEqual({
|
expect(config.FIAT_PRICE).toStrictEqual({
|
||||||
ENABLED: true,
|
ENABLED: true,
|
||||||
|
PAID: false,
|
||||||
API_KEY: '',
|
API_KEY: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test-
|
|||||||
|
|
||||||
describe('Rust GBT', () => {
|
describe('Rust GBT', () => {
|
||||||
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
|
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
|
||||||
const rustGbt = new GbtGenerator();
|
const rustGbt = new GbtGenerator(4_000_000, 8);
|
||||||
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
|
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
|
||||||
const result = await rustGbt.make(mempool, [], maxUid);
|
const result = await rustGbt.make(mempool, [], maxUid);
|
||||||
|
|
||||||
|
|||||||
87
backend/src/api/about.routes.ts
Normal file
87
backend/src/api/about.routes.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Application } from "express";
|
||||||
|
import config from "../config";
|
||||||
|
import axios from "axios";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
|
class AboutRoutes {
|
||||||
|
public initRoutes(app: Application) {
|
||||||
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
||||||
|
responseType: 'stream', timeout: 10000
|
||||||
|
});
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
||||||
|
responseType: 'stream', timeout: 10000
|
||||||
|
});
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
||||||
|
responseType: 'stream', timeout: 10000
|
||||||
|
});
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => {
|
||||||
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to fetch sponsors from ${url}. ${e}`, 'About Page');
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
|
||||||
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to fetch sponsor profile image from ${url}. ${e}`, 'About Page');
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AboutRoutes();
|
||||||
@@ -1,738 +0,0 @@
|
|||||||
import logger from '../logger';
|
|
||||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
|
||||||
|
|
||||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
|
||||||
const BLOCK_SIGOPS = 80_000;
|
|
||||||
const MAX_RELATIVE_GRAPH_SIZE = 200;
|
|
||||||
const BID_BOOST_WINDOW = 40_000;
|
|
||||||
const BID_BOOST_MIN_OFFSET = 10_000;
|
|
||||||
const BID_BOOST_MAX_OFFSET = 400_000;
|
|
||||||
|
|
||||||
type Acceleration = {
|
|
||||||
txid: string;
|
|
||||||
max_bid: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TxSummary {
|
|
||||||
txid: string; // txid of the current transaction
|
|
||||||
effectiveVsize: number; // Total vsize of the dependency tree
|
|
||||||
effectiveFee: number; // Total fee of the dependency tree in sats
|
|
||||||
ancestorCount: number; // Number of ancestors
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccelerationInfo {
|
|
||||||
txSummary: TxSummary;
|
|
||||||
targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
|
|
||||||
nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
|
|
||||||
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GraphTx {
|
|
||||||
txid: string;
|
|
||||||
vsize: number;
|
|
||||||
weight: number;
|
|
||||||
fees: {
|
|
||||||
base: number; // in sats
|
|
||||||
};
|
|
||||||
depends: string[];
|
|
||||||
spentby: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MempoolTx extends GraphTx {
|
|
||||||
ancestorcount: number;
|
|
||||||
ancestorsize: number;
|
|
||||||
fees: { // in sats
|
|
||||||
base: number;
|
|
||||||
ancestor: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
ancestors: Map<string, MempoolTx>,
|
|
||||||
ancestorRate: number;
|
|
||||||
individualRate: number;
|
|
||||||
score: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccelerationCosts {
|
|
||||||
/**
|
|
||||||
* Takes a list of accelerations and verbose block data
|
|
||||||
* Returns the "fair" boost rate to charge accelerations
|
|
||||||
*
|
|
||||||
* @param accelerationsx
|
|
||||||
* @param verboseBlock
|
|
||||||
*/
|
|
||||||
public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
|
|
||||||
// Run GBT ourselves to calculate accurate effective fee rates
|
|
||||||
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
|
|
||||||
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
|
|
||||||
|
|
||||||
// initialize working maps for fast tx lookups
|
|
||||||
const accMap = {};
|
|
||||||
const txMap = {};
|
|
||||||
for (const acceleration of accelerations) {
|
|
||||||
accMap[acceleration.txid] = acceleration;
|
|
||||||
}
|
|
||||||
for (const tx of template) {
|
|
||||||
txMap[tx.txid] = tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identify and exclude accelerated and otherwise prioritized transactions
|
|
||||||
const excludeMap = {};
|
|
||||||
let totalWeight = 0;
|
|
||||||
let minAcceleratedPackage = Infinity;
|
|
||||||
let lastEffectiveRate = 0;
|
|
||||||
// Iterate over the mined template from bottom to top.
|
|
||||||
// Transactions should appear in ascending order of mining priority.
|
|
||||||
for (const blockTx of [...blockTxs].reverse()) {
|
|
||||||
const txid = blockTx.txid;
|
|
||||||
const tx = txMap[txid];
|
|
||||||
totalWeight += tx.weight;
|
|
||||||
const isAccelerated = accMap[txid] != null;
|
|
||||||
// If a cluster has a in-band effective fee rate than the previous cluster,
|
|
||||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
|
||||||
// so exclude from the analysis.
|
|
||||||
const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
|
|
||||||
if (isPrioritized || isAccelerated) {
|
|
||||||
let packageWeight = 0;
|
|
||||||
// exclude this whole CPFP cluster
|
|
||||||
for (const clusterTxid of tx.cluster) {
|
|
||||||
packageWeight += txMap[clusterTxid].weight;
|
|
||||||
if (!excludeMap[clusterTxid]) {
|
|
||||||
excludeMap[clusterTxid] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// keep track of the smallest accelerated CPFP cluster for later
|
|
||||||
if (isAccelerated) {
|
|
||||||
minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isPrioritized) {
|
|
||||||
if (!isAccelerated) {
|
|
||||||
lastEffectiveRate = tx.effectiveFeePerVsize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
|
|
||||||
// where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
|
|
||||||
// then taking the average fee rate of the following BID_BOOST_WINDOW weight units
|
|
||||||
// (ignoring accelerated transactions and their ancestors).
|
|
||||||
//
|
|
||||||
// Transactions within the offset might pay less than the fair rate due to bin-packing effects
|
|
||||||
// But the average rate paid by the next chunk of non-accelerated transactions provides a good
|
|
||||||
// upper bound on the "next best rate" of alternatives to including the accelerated transactions
|
|
||||||
// (since, if there were any better options, they would have been included instead)
|
|
||||||
const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
|
|
||||||
const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
|
|
||||||
const leftBound = windowOffset;
|
|
||||||
const rightBound = windowOffset + BID_BOOST_WINDOW;
|
|
||||||
let totalFeeInWindow = 0;
|
|
||||||
let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
|
|
||||||
let txIndex = blockTxs.length - 1;
|
|
||||||
for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
|
|
||||||
const txid = blockTxs[txIndex].txid;
|
|
||||||
const tx = txMap[txid];
|
|
||||||
if (excludeMap[txid]) {
|
|
||||||
// skip prioritized transactions and their ancestors
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = offset;
|
|
||||||
const right = offset + tx.weight;
|
|
||||||
offset += tx.weight;
|
|
||||||
if (right < leftBound) {
|
|
||||||
// not within window yet
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (left > rightBound) {
|
|
||||||
// past window
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// count fees for weight units within the window
|
|
||||||
const overlapLeft = Math.max(leftBound, left);
|
|
||||||
const overlapRight = Math.min(rightBound, right);
|
|
||||||
const overlapUnits = overlapRight - overlapLeft;
|
|
||||||
totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
|
|
||||||
totalWeightInWindow += overlapUnits;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeightInWindow < BID_BOOST_WINDOW) {
|
|
||||||
// not enough un-prioritized transactions to calculate a fair rate
|
|
||||||
// just charge everyone their max bids
|
|
||||||
return Infinity;
|
|
||||||
}
|
|
||||||
// Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
|
|
||||||
const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
|
|
||||||
return averageRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an accelerated mined txid and a target rate
|
|
||||||
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
|
|
||||||
*
|
|
||||||
* @param txid
|
|
||||||
* @param medianFeeRate
|
|
||||||
*/
|
|
||||||
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
|
|
||||||
// Get same-block transaction ancestors
|
|
||||||
const allRelatives = this.getSameBlockRelatives(tx, transactions);
|
|
||||||
const relativesMap = this.initializeRelatives(allRelatives);
|
|
||||||
const rootTx = relativesMap.get(tx.txid) as MempoolTx;
|
|
||||||
|
|
||||||
// Calculate cost to boost
|
|
||||||
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a raw transaction, and builds a graph of same-block relatives,
|
|
||||||
* and returns as a MempoolTx
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
*/
|
|
||||||
private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
|
||||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
|
||||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
|
||||||
for (const tx of transactions) {
|
|
||||||
blockTxs.set(tx.txid, tx);
|
|
||||||
for (const vin of tx.vin) {
|
|
||||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const relatives: Map<string, GraphTx> = new Map();
|
|
||||||
const stack: string[] = [tx.txid];
|
|
||||||
|
|
||||||
// build set of same-block ancestors
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const nextTxid = stack.pop();
|
|
||||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
|
||||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mempoolTx = this.convertToGraphTx(nextTx);
|
|
||||||
|
|
||||||
mempoolTx.fees.base = nextTx.fee || 0;
|
|
||||||
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
|
|
||||||
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
|
|
||||||
|
|
||||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
|
||||||
if (txid) {
|
|
||||||
stack.push(txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relatives.set(mempoolTx.txid, mempoolTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
return relatives;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a raw transaction and converts it to MempoolTx format
|
|
||||||
* fee and ancestor data is initialized with dummy/null values
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
*/
|
|
||||||
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
|
||||||
return {
|
|
||||||
txid: tx.txid,
|
|
||||||
vsize: Math.ceil(tx.weight / 4),
|
|
||||||
weight: tx.weight,
|
|
||||||
fees: {
|
|
||||||
base: 0, // dummy
|
|
||||||
},
|
|
||||||
depends: [], // dummy
|
|
||||||
spentby: [], //dummy
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
|
|
||||||
return {
|
|
||||||
...tx,
|
|
||||||
fees: {
|
|
||||||
base: tx.fees.base,
|
|
||||||
ancestor: tx.fees.base,
|
|
||||||
},
|
|
||||||
ancestorcount: 1,
|
|
||||||
ancestorsize: Math.ceil(tx.weight / 4),
|
|
||||||
ancestors: new Map<string, MempoolTx>(),
|
|
||||||
ancestorRate: 0,
|
|
||||||
individualRate: 0,
|
|
||||||
score: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
|
|
||||||
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
* @param ancestors
|
|
||||||
*/
|
|
||||||
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
|
|
||||||
// add root tx to the ancestor map
|
|
||||||
relatives.set(tx.txid, tx);
|
|
||||||
|
|
||||||
// Check for high-sigop transactions (not supported)
|
|
||||||
relatives.forEach(entry => {
|
|
||||||
if (entry.vsize > Math.ceil(entry.weight / 4)) {
|
|
||||||
throw new Error(`high_sigop_tx`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize individual & ancestor fee rates
|
|
||||||
relatives.forEach(entry => this.setAncestorScores(entry));
|
|
||||||
|
|
||||||
// Sort by descending ancestor score
|
|
||||||
let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
|
||||||
|
|
||||||
let includedInCluster: Map<string, MempoolTx> | null = null;
|
|
||||||
|
|
||||||
// While highest score >= targetFeeRate
|
|
||||||
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
|
|
||||||
while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
|
|
||||||
maxIterations--;
|
|
||||||
// Grab the highest scoring entry
|
|
||||||
const best = sortedRelatives.shift();
|
|
||||||
if (best) {
|
|
||||||
const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
|
|
||||||
if (best.ancestors.has(tx.txid)) {
|
|
||||||
includedInCluster = cluster;
|
|
||||||
}
|
|
||||||
cluster.set(best.txid, best);
|
|
||||||
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
|
|
||||||
// and update scores, ancestor totals and dependencies for the survivors
|
|
||||||
this.removeAncestors(cluster, relatives);
|
|
||||||
|
|
||||||
// re-sort
|
|
||||||
sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity check for infinite loops / too many ancestors (should never happen)
|
|
||||||
if (maxIterations <= 0) {
|
|
||||||
logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
|
|
||||||
throw new Error('invalid_tx_dependencies');
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalFee = tx.fees.ancestor;
|
|
||||||
|
|
||||||
// transaction is already CPFP-d above the target rate by some descendant
|
|
||||||
if (includedInCluster) {
|
|
||||||
let clusterSize = 0;
|
|
||||||
let clusterFee = 0;
|
|
||||||
includedInCluster.forEach(entry => {
|
|
||||||
clusterSize += entry.vsize;
|
|
||||||
clusterFee += entry.fees.base;
|
|
||||||
});
|
|
||||||
const clusterRate = clusterFee / clusterSize;
|
|
||||||
totalFee = Math.ceil(tx.ancestorsize * clusterRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
|
|
||||||
// Cost = (totalVsize * targetFeeRate) - totalFee
|
|
||||||
return {
|
|
||||||
txSummary: {
|
|
||||||
txid: tx.txid,
|
|
||||||
effectiveVsize: tx.ancestorsize,
|
|
||||||
effectiveFee: totalFee,
|
|
||||||
ancestorCount: tx.ancestorcount,
|
|
||||||
},
|
|
||||||
cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
|
|
||||||
targetFeeRate,
|
|
||||||
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
|
||||||
* for each transaction.
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
* @param all
|
|
||||||
*/
|
|
||||||
private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
|
|
||||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
|
||||||
if (depth >= 100) {
|
|
||||||
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
|
|
||||||
throw new Error('invalid_tx_dependencies');
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the ancestor map for this tx
|
|
||||||
tx.ancestors = new Map<string, MempoolTx>();
|
|
||||||
tx.depends.forEach(parentId => {
|
|
||||||
const parent = all.get(parentId);
|
|
||||||
if (parent) {
|
|
||||||
// add the parent
|
|
||||||
tx.ancestors?.set(parentId, parent);
|
|
||||||
// check for a cached copy of this parent's ancestors
|
|
||||||
let ancestors = visited.get(parent.txid);
|
|
||||||
if (!ancestors) {
|
|
||||||
// recursively fetch the parent's ancestors
|
|
||||||
ancestors = this.setAncestors(parent, all, visited, depth + 1);
|
|
||||||
}
|
|
||||||
// and add to this tx's map
|
|
||||||
ancestors.forEach((ancestor, ancestorId) => {
|
|
||||||
tx.ancestors?.set(ancestorId, ancestor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
visited.set(tx.txid, tx.ancestors);
|
|
||||||
|
|
||||||
return tx.ancestors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
|
||||||
* by running setAncestors on each leaf, and caching intermediate results.
|
|
||||||
* then initializes ancestor data for each transaction
|
|
||||||
*
|
|
||||||
* @param all
|
|
||||||
*/
|
|
||||||
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
|
|
||||||
const mempoolTxs = new Map<string, MempoolTx>();
|
|
||||||
all.forEach(entry => {
|
|
||||||
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
|
|
||||||
});
|
|
||||||
const visited: Map<string, Map<string, MempoolTx>> = new Map();
|
|
||||||
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
|
||||||
for (const leaf of leaves) {
|
|
||||||
this.setAncestors(leaf, mempoolTxs, visited);
|
|
||||||
}
|
|
||||||
mempoolTxs.forEach(entry => {
|
|
||||||
entry.ancestors?.forEach(ancestor => {
|
|
||||||
entry.ancestorcount++;
|
|
||||||
entry.ancestorsize += ancestor.vsize;
|
|
||||||
entry.fees.ancestor += ancestor.fees.base;
|
|
||||||
});
|
|
||||||
this.setAncestorScores(entry);
|
|
||||||
});
|
|
||||||
return mempoolTxs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
|
||||||
* and update the survivors' scores and ancestors
|
|
||||||
*
|
|
||||||
* @param cluster
|
|
||||||
* @param ancestors
|
|
||||||
*/
|
|
||||||
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
|
|
||||||
// remove
|
|
||||||
cluster.forEach(tx => {
|
|
||||||
all.delete(tx.txid);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update survivors
|
|
||||||
all.forEach(tx => {
|
|
||||||
cluster.forEach(remove => {
|
|
||||||
if (tx.ancestors?.has(remove.txid)) {
|
|
||||||
// remove as dependency
|
|
||||||
tx.ancestors.delete(remove.txid);
|
|
||||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
|
||||||
// update ancestor sizes and fees
|
|
||||||
tx.ancestorsize -= remove.vsize;
|
|
||||||
tx.fees.ancestor -= remove.fees.base;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// recalculate fee rates
|
|
||||||
this.setAncestorScores(tx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
*/
|
|
||||||
private setAncestorScores(tx: MempoolTx): void {
|
|
||||||
tx.individualRate = tx.fees.base / tx.vsize;
|
|
||||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
|
||||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by descending score
|
|
||||||
private mempoolComparator(a, b): number {
|
|
||||||
return b.score - a.score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new AccelerationCosts;
|
|
||||||
|
|
||||||
interface TemplateTransaction {
|
|
||||||
txid: string;
|
|
||||||
order: number;
|
|
||||||
weight: number;
|
|
||||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
|
||||||
sigops: number;
|
|
||||||
fee: number;
|
|
||||||
feeDelta: number;
|
|
||||||
ancestors: string[];
|
|
||||||
cluster: string[];
|
|
||||||
effectiveFeePerVsize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MinerTransaction extends TemplateTransaction {
|
|
||||||
inputs: string[];
|
|
||||||
feePerVsize: number;
|
|
||||||
relativesSet: boolean;
|
|
||||||
ancestorMap: Map<string, MinerTransaction>;
|
|
||||||
children: Set<MinerTransaction>;
|
|
||||||
ancestorFee: number;
|
|
||||||
ancestorVsize: number;
|
|
||||||
ancestorSigops: number;
|
|
||||||
score: number;
|
|
||||||
used: boolean;
|
|
||||||
modified: boolean;
|
|
||||||
dependencyRate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
|
||||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
|
||||||
*/
|
|
||||||
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
|
||||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
|
||||||
const mempoolArray: MinerTransaction[] = [];
|
|
||||||
|
|
||||||
candidates.forEach(tx => {
|
|
||||||
// initializing everything up front helps V8 optimize property access later
|
|
||||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
|
||||||
const feePerVsize = (tx.fee / adjustedVsize);
|
|
||||||
auditPool.set(tx.txid, {
|
|
||||||
txid: tx.txid,
|
|
||||||
order: txidToOrdering(tx.txid),
|
|
||||||
fee: tx.fee,
|
|
||||||
feeDelta: 0,
|
|
||||||
weight: tx.weight,
|
|
||||||
adjustedVsize,
|
|
||||||
feePerVsize: feePerVsize,
|
|
||||||
effectiveFeePerVsize: feePerVsize,
|
|
||||||
dependencyRate: feePerVsize,
|
|
||||||
sigops: tx.sigops || 0,
|
|
||||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
|
||||||
relativesSet: false,
|
|
||||||
ancestors: [],
|
|
||||||
cluster: [],
|
|
||||||
ancestorMap: new Map<string, MinerTransaction>(),
|
|
||||||
children: new Set<MinerTransaction>(),
|
|
||||||
ancestorFee: 0,
|
|
||||||
ancestorVsize: 0,
|
|
||||||
ancestorSigops: 0,
|
|
||||||
score: 0,
|
|
||||||
used: false,
|
|
||||||
modified: false,
|
|
||||||
});
|
|
||||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
|
||||||
});
|
|
||||||
|
|
||||||
// set accelerated effective fee
|
|
||||||
for (const acceleration of accelerations) {
|
|
||||||
const tx = auditPool.get(acceleration.txid);
|
|
||||||
if (tx) {
|
|
||||||
tx.feeDelta = acceleration.max_bid;
|
|
||||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
|
||||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
|
||||||
tx.dependencyRate = tx.feePerVsize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build relatives graph & calculate ancestor scores
|
|
||||||
for (const tx of mempoolArray) {
|
|
||||||
if (!tx.relativesSet) {
|
|
||||||
setRelatives(tx, auditPool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by descending ancestor score
|
|
||||||
mempoolArray.sort(priorityComparator);
|
|
||||||
|
|
||||||
// Build blocks by greedily choosing the highest feerate package
|
|
||||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
|
||||||
const blocks: number[][] = [];
|
|
||||||
let blockWeight = 0;
|
|
||||||
let blockSigops = 0;
|
|
||||||
const transactions: MinerTransaction[] = [];
|
|
||||||
let modified: MinerTransaction[] = [];
|
|
||||||
const overflow: MinerTransaction[] = [];
|
|
||||||
let failures = 0;
|
|
||||||
while (mempoolArray.length || modified.length) {
|
|
||||||
// skip invalid transactions
|
|
||||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
|
||||||
mempoolArray.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select best next package
|
|
||||||
let nextTx;
|
|
||||||
const nextPoolTx = mempoolArray[0];
|
|
||||||
const nextModifiedTx = modified[0];
|
|
||||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
|
||||||
nextTx = nextPoolTx;
|
|
||||||
mempoolArray.shift();
|
|
||||||
} else {
|
|
||||||
modified.shift();
|
|
||||||
if (nextModifiedTx) {
|
|
||||||
nextTx = nextModifiedTx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextTx && !nextTx?.used) {
|
|
||||||
// Check if the package fits into this block
|
|
||||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
|
||||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
|
||||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
|
||||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
|
||||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
|
||||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
|
||||||
const used: MinerTransaction[] = [];
|
|
||||||
while (sortedTxSet.length) {
|
|
||||||
const ancestor = sortedTxSet.pop();
|
|
||||||
if (!ancestor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ancestor.used = true;
|
|
||||||
ancestor.usedBy = nextTx.txid;
|
|
||||||
// update this tx with effective fee rate & relatives data
|
|
||||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
|
||||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
|
||||||
}
|
|
||||||
ancestor.cluster = clusterTxids;
|
|
||||||
transactions.push(ancestor);
|
|
||||||
blockWeight += ancestor.weight;
|
|
||||||
blockSigops += ancestor.sigops;
|
|
||||||
used.push(ancestor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
|
||||||
if (used.length) {
|
|
||||||
used.forEach(tx => {
|
|
||||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
failures = 0;
|
|
||||||
} else {
|
|
||||||
// hold this package in an overflow list while we check for smaller options
|
|
||||||
overflow.push(nextTx);
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this block is full
|
|
||||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
|
||||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
|
||||||
|
|
||||||
if (exceededPackageTries || queueEmpty) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tx of transactions) {
|
|
||||||
tx.ancestors = Object.values(tx.ancestorMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// traverse in-mempool ancestors
|
|
||||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
|
||||||
function setRelatives(
|
|
||||||
tx: MinerTransaction,
|
|
||||||
mempool: Map<string, MinerTransaction>,
|
|
||||||
): void {
|
|
||||||
for (const parent of tx.inputs) {
|
|
||||||
const parentTx = mempool.get(parent);
|
|
||||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
|
||||||
tx.ancestorMap.set(parent, parentTx);
|
|
||||||
parentTx.children.add(tx);
|
|
||||||
// visit each node only once
|
|
||||||
if (!parentTx.relativesSet) {
|
|
||||||
setRelatives(parentTx, mempool);
|
|
||||||
}
|
|
||||||
parentTx.ancestorMap.forEach((ancestor) => {
|
|
||||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
|
||||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
|
||||||
tx.ancestorSigops = tx.sigops || 0;
|
|
||||||
tx.ancestorMap.forEach((ancestor) => {
|
|
||||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
|
||||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
|
||||||
tx.ancestorSigops += ancestor.sigops;
|
|
||||||
});
|
|
||||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
|
||||||
tx.relativesSet = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
|
||||||
// avoids recursion to limit call stack depth
|
|
||||||
function updateDescendants(
|
|
||||||
rootTx: MinerTransaction,
|
|
||||||
mempool: Map<string, MinerTransaction>,
|
|
||||||
modified: MinerTransaction[],
|
|
||||||
clusterRate: number,
|
|
||||||
): MinerTransaction[] {
|
|
||||||
const descendantSet: Set<MinerTransaction> = new Set();
|
|
||||||
// stack of nodes left to visit
|
|
||||||
const descendants: MinerTransaction[] = [];
|
|
||||||
let descendantTx: MinerTransaction | undefined;
|
|
||||||
rootTx.children.forEach(childTx => {
|
|
||||||
if (!descendantSet.has(childTx)) {
|
|
||||||
descendants.push(childTx);
|
|
||||||
descendantSet.add(childTx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
while (descendants.length) {
|
|
||||||
descendantTx = descendants.pop();
|
|
||||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
|
||||||
// remove tx as ancestor
|
|
||||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
|
||||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
|
||||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
|
||||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
|
||||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
|
||||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
|
||||||
|
|
||||||
if (!descendantTx.modified) {
|
|
||||||
descendantTx.modified = true;
|
|
||||||
modified.push(descendantTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add this node's children to the stack
|
|
||||||
descendantTx.children.forEach(childTx => {
|
|
||||||
// visit each node only once
|
|
||||||
if (!descendantSet.has(childTx)) {
|
|
||||||
descendants.push(childTx);
|
|
||||||
descendantSet.add(childTx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return new, resorted modified list
|
|
||||||
return modified.sort(priorityComparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
|
||||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
|
||||||
if (b.score === a.score) {
|
|
||||||
// tie-break by txid for stability
|
|
||||||
return a.order - b.order;
|
|
||||||
} else {
|
|
||||||
return b.score - a.score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns the most significant 4 bytes of the txid as an integer
|
|
||||||
function txidToOrdering(txid: string): number {
|
|
||||||
return parseInt(
|
|
||||||
txid.substring(62, 64) +
|
|
||||||
txid.substring(60, 62) +
|
|
||||||
txid.substring(58, 60) +
|
|
||||||
txid.substring(56, 58),
|
|
||||||
16
|
|
||||||
);
|
|
||||||
}
|
|
||||||
84
backend/src/api/acceleration/acceleration.routes.ts
Normal file
84
backend/src/api/acceleration/acceleration.routes.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Application, Request, Response } from 'express';
|
||||||
|
import config from '../../config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import mempool from '../mempool';
|
||||||
|
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||||
|
|
||||||
|
class AccelerationRoutes {
|
||||||
|
private tag = 'Accelerator';
|
||||||
|
|
||||||
|
public initRoutes(app: Application): void {
|
||||||
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
|
||||||
|
.post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getAcceleratorAccelerations(req: Request, res: Response): Promise<void> {
|
||||||
|
const accelerations = mempool.getAccelerations();
|
||||||
|
res.status(200).send(Object.values(accelerations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> {
|
||||||
|
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
|
||||||
|
res.status(200).send(history.map(accel => ({
|
||||||
|
txid: accel.txid,
|
||||||
|
added: accel.added,
|
||||||
|
status: 'completed',
|
||||||
|
effectiveFee: accel.effective_fee,
|
||||||
|
effectiveVsize: accel.effective_vsize,
|
||||||
|
boostRate: accel.boost_rate,
|
||||||
|
boostCost: accel.boost_cost,
|
||||||
|
blockHeight: accel.height,
|
||||||
|
pools: [accel.pool],
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise<void> {
|
||||||
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||||
|
for (const key in response.headers) {
|
||||||
|
res.setHeader(key, response.headers[key]);
|
||||||
|
}
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise<void> {
|
||||||
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||||
|
for (const key in response.headers) {
|
||||||
|
res.setHeader(key, response.headers[key]);
|
||||||
|
}
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> {
|
||||||
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 });
|
||||||
|
for (const key in response.headers) {
|
||||||
|
res.setHeader(key, response.headers[key]);
|
||||||
|
}
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag);
|
||||||
|
res.status(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AccelerationRoutes();
|
||||||
244
backend/src/api/acceleration/acceleration.ts
Normal file
244
backend/src/api/acceleration/acceleration.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import logger from '../../logger';
|
||||||
|
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||||
|
import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner';
|
||||||
|
|
||||||
|
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||||
|
const MAX_RELATIVE_GRAPH_SIZE = 200;
|
||||||
|
const BID_BOOST_WINDOW = 40_000;
|
||||||
|
const BID_BOOST_MIN_OFFSET = 10_000;
|
||||||
|
const BID_BOOST_MAX_OFFSET = 400_000;
|
||||||
|
|
||||||
|
export type Acceleration = {
|
||||||
|
txid: string;
|
||||||
|
max_bid: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TxSummary {
|
||||||
|
txid: string; // txid of the current transaction
|
||||||
|
effectiveVsize: number; // Total vsize of the dependency tree
|
||||||
|
effectiveFee: number; // Total fee of the dependency tree in sats
|
||||||
|
ancestorCount: number; // Number of ancestors
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccelerationInfo {
|
||||||
|
txSummary: TxSummary;
|
||||||
|
targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
|
||||||
|
nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
|
||||||
|
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccelerationCosts {
|
||||||
|
/**
|
||||||
|
* Takes a list of accelerations and verbose block data
|
||||||
|
* Returns the "fair" boost rate to charge accelerations
|
||||||
|
*
|
||||||
|
* @param accelerationsx
|
||||||
|
* @param verboseBlock
|
||||||
|
*/
|
||||||
|
public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number {
|
||||||
|
// Run GBT ourselves to calculate accurate effective fee rates
|
||||||
|
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
|
||||||
|
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
|
||||||
|
|
||||||
|
// initialize working maps for fast tx lookups
|
||||||
|
const accMap = {};
|
||||||
|
const txMap = {};
|
||||||
|
for (const acceleration of accelerations) {
|
||||||
|
accMap[acceleration.txid] = acceleration;
|
||||||
|
}
|
||||||
|
for (const tx of template) {
|
||||||
|
txMap[tx.txid] = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify and exclude accelerated and otherwise prioritized transactions
|
||||||
|
const excludeMap = {};
|
||||||
|
let totalWeight = 0;
|
||||||
|
let minAcceleratedPackage = Infinity;
|
||||||
|
let lastEffectiveRate = 0;
|
||||||
|
// Iterate over the mined template from bottom to top.
|
||||||
|
// Transactions should appear in ascending order of mining priority.
|
||||||
|
for (const blockTx of [...blockTxs].reverse()) {
|
||||||
|
const txid = blockTx.txid;
|
||||||
|
const tx = txMap[txid];
|
||||||
|
totalWeight += tx.weight;
|
||||||
|
const isAccelerated = accMap[txid] != null;
|
||||||
|
// If a cluster has a in-band effective fee rate than the previous cluster,
|
||||||
|
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||||
|
// so exclude from the analysis.
|
||||||
|
const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
|
||||||
|
if (isPrioritized || isAccelerated) {
|
||||||
|
let packageWeight = 0;
|
||||||
|
// exclude this whole CPFP cluster
|
||||||
|
for (const clusterTxid of tx.cluster) {
|
||||||
|
packageWeight += txMap[clusterTxid].weight;
|
||||||
|
if (!excludeMap[clusterTxid]) {
|
||||||
|
excludeMap[clusterTxid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// keep track of the smallest accelerated CPFP cluster for later
|
||||||
|
if (isAccelerated) {
|
||||||
|
minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isPrioritized) {
|
||||||
|
if (!isAccelerated) {
|
||||||
|
lastEffectiveRate = tx.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
|
||||||
|
// where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
|
||||||
|
// then taking the average fee rate of the following BID_BOOST_WINDOW weight units
|
||||||
|
// (ignoring accelerated transactions and their ancestors).
|
||||||
|
//
|
||||||
|
// Transactions within the offset might pay less than the fair rate due to bin-packing effects
|
||||||
|
// But the average rate paid by the next chunk of non-accelerated transactions provides a good
|
||||||
|
// upper bound on the "next best rate" of alternatives to including the accelerated transactions
|
||||||
|
// (since, if there were any better options, they would have been included instead)
|
||||||
|
const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
|
||||||
|
const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
|
||||||
|
const leftBound = windowOffset;
|
||||||
|
const rightBound = windowOffset + BID_BOOST_WINDOW;
|
||||||
|
let totalFeeInWindow = 0;
|
||||||
|
let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
|
||||||
|
let txIndex = blockTxs.length - 1;
|
||||||
|
for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
|
||||||
|
const txid = blockTxs[txIndex].txid;
|
||||||
|
const tx = txMap[txid];
|
||||||
|
if (excludeMap[txid]) {
|
||||||
|
// skip prioritized transactions and their ancestors
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = offset;
|
||||||
|
const right = offset + tx.weight;
|
||||||
|
offset += tx.weight;
|
||||||
|
if (right < leftBound) {
|
||||||
|
// not within window yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (left > rightBound) {
|
||||||
|
// past window
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// count fees for weight units within the window
|
||||||
|
const overlapLeft = Math.max(leftBound, left);
|
||||||
|
const overlapRight = Math.min(rightBound, right);
|
||||||
|
const overlapUnits = overlapRight - overlapLeft;
|
||||||
|
totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
|
||||||
|
totalWeightInWindow += overlapUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeightInWindow < BID_BOOST_WINDOW) {
|
||||||
|
// not enough un-prioritized transactions to calculate a fair rate
|
||||||
|
// just charge everyone their max bids
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
// Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
|
||||||
|
const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
|
||||||
|
return averageRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes an accelerated mined txid and a target rate
|
||||||
|
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
|
||||||
|
*
|
||||||
|
* @param txid
|
||||||
|
* @param medianFeeRate
|
||||||
|
*/
|
||||||
|
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
|
||||||
|
// Get same-block transaction ancestors
|
||||||
|
const allRelatives = getSameBlockRelatives(tx, transactions);
|
||||||
|
const relativesMap = initializeRelatives(allRelatives);
|
||||||
|
const rootTx = relativesMap.get(tx.txid) as GraphTx;
|
||||||
|
|
||||||
|
// Calculate cost to boost
|
||||||
|
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
|
||||||
|
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
|
||||||
|
*
|
||||||
|
* @param tx
|
||||||
|
* @param ancestors
|
||||||
|
*/
|
||||||
|
private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo {
|
||||||
|
// add root tx to the ancestor map
|
||||||
|
relatives.set(tx.txid, tx);
|
||||||
|
|
||||||
|
// Check for high-sigop transactions (not supported)
|
||||||
|
relatives.forEach(entry => {
|
||||||
|
if (entry.vsize > Math.ceil(entry.weight / 4)) {
|
||||||
|
throw new Error(`high_sigop_tx`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize individual & ancestor fee rates
|
||||||
|
relatives.forEach(entry => setAncestorScores(entry));
|
||||||
|
|
||||||
|
// Sort by descending ancestor score
|
||||||
|
let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
|
||||||
|
|
||||||
|
let includedInCluster: Map<string, GraphTx> | null = null;
|
||||||
|
|
||||||
|
// While highest score >= targetFeeRate
|
||||||
|
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
|
||||||
|
while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
|
||||||
|
maxIterations--;
|
||||||
|
// Grab the highest scoring entry
|
||||||
|
const best = sortedRelatives.shift();
|
||||||
|
if (best) {
|
||||||
|
const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []);
|
||||||
|
if (best.ancestors.has(tx.txid)) {
|
||||||
|
includedInCluster = cluster;
|
||||||
|
}
|
||||||
|
cluster.set(best.txid, best);
|
||||||
|
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
|
||||||
|
// and update scores, ancestor totals and dependencies for the survivors
|
||||||
|
removeAncestors(cluster, relatives);
|
||||||
|
|
||||||
|
// re-sort
|
||||||
|
sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity check for infinite loops / too many ancestors (should never happen)
|
||||||
|
if (maxIterations <= 0) {
|
||||||
|
logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
|
||||||
|
throw new Error('invalid_tx_dependencies');
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalFee = tx.fees.ancestor;
|
||||||
|
|
||||||
|
// transaction is already CPFP-d above the target rate by some descendant
|
||||||
|
if (includedInCluster) {
|
||||||
|
let clusterSize = 0;
|
||||||
|
let clusterFee = 0;
|
||||||
|
includedInCluster.forEach(entry => {
|
||||||
|
clusterSize += entry.vsize;
|
||||||
|
clusterFee += entry.fees.base;
|
||||||
|
});
|
||||||
|
const clusterRate = clusterFee / clusterSize;
|
||||||
|
totalFee = Math.ceil(tx.ancestorsize * clusterRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
|
||||||
|
// Cost = (totalVsize * targetFeeRate) - totalFee
|
||||||
|
return {
|
||||||
|
txSummary: {
|
||||||
|
txid: tx.txid,
|
||||||
|
effectiveVsize: tx.ancestorsize,
|
||||||
|
effectiveFee: totalFee,
|
||||||
|
ancestorCount: tx.ancestorcount,
|
||||||
|
},
|
||||||
|
cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
|
||||||
|
targetFeeRate,
|
||||||
|
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AccelerationCosts;
|
||||||
@@ -2,24 +2,28 @@ import config from '../config';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
||||||
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||||
|
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||||
|
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
|
const isAccelerated = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
let matchedWeight = 0;
|
let matchedWeight = 0;
|
||||||
let projectedWeight = 0;
|
let projectedWeight = 0;
|
||||||
@@ -32,6 +36,7 @@ class Audit {
|
|||||||
inBlock[tx.txid] = tx;
|
inBlock[tx.txid] = tx;
|
||||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||||
accelerated.push(tx.txid);
|
accelerated.push(tx.txid);
|
||||||
|
isAccelerated[tx.txid] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// coinbase is always expected
|
// coinbase is always expected
|
||||||
@@ -75,10 +80,6 @@ class Audit {
|
|||||||
let failures = 0;
|
let failures = 0;
|
||||||
let blockIndex = 1;
|
let blockIndex = 1;
|
||||||
while (projectedBlocks[blockIndex] && failures < 500) {
|
while (projectedBlocks[blockIndex] && failures < 500) {
|
||||||
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
|
|
||||||
index = 0;
|
|
||||||
blockIndex++;
|
|
||||||
}
|
|
||||||
const txid = projectedBlocks[blockIndex].transactionIds[index];
|
const txid = projectedBlocks[blockIndex].transactionIds[index];
|
||||||
const tx = mempool[txid];
|
const tx = mempool[txid];
|
||||||
if (tx) {
|
if (tx) {
|
||||||
@@ -102,6 +103,10 @@ class Audit {
|
|||||||
logger.warn('projected transaction missing from mempool cache');
|
logger.warn('projected transaction missing from mempool cache');
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
|
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
|
||||||
|
index = 0;
|
||||||
|
blockIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark unexpected transactions in the mined block as 'added'
|
// mark unexpected transactions in the mined block as 'added'
|
||||||
@@ -113,11 +118,16 @@ class Audit {
|
|||||||
} else {
|
} else {
|
||||||
if (rbfCache.has(tx.txid)) {
|
if (rbfCache.has(tx.txid)) {
|
||||||
rbf.push(tx.txid);
|
rbf.push(tx.txid);
|
||||||
} else if (!isDisplaced[tx.txid]) {
|
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
|
||||||
|
unseen.push(tx.txid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (mempool[tx.txid]) {
|
if (mempool[tx.txid]) {
|
||||||
prioritized.push(tx.txid);
|
if (isDisplaced[tx.txid]) {
|
||||||
|
added.push(tx.txid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
added.push(tx.txid);
|
unseen.push(tx.txid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overflowWeight += tx.weight;
|
overflowWeight += tx.weight;
|
||||||
@@ -125,6 +135,8 @@ class Audit {
|
|||||||
totalWeight += tx.weight;
|
totalWeight += tx.weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||||
|
|
||||||
// transactions missing from near the end of our template are probably not being censored
|
// transactions missing from near the end of our template are probably not being censored
|
||||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||||
let maxOverflowRate = 0;
|
let maxOverflowRate = 0;
|
||||||
@@ -165,6 +177,7 @@ class Audit {
|
|||||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
unseen,
|
||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
prioritized,
|
prioritized,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
@@ -22,11 +22,13 @@ export interface AbstractBitcoinApi {
|
|||||||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
|
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||||
|
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
|
||||||
|
|
||||||
startHealthChecks(): void;
|
startHealthChecks(): void;
|
||||||
getHealthStatus(): HealthCheckHost[];
|
getHealthStatus(): HealthCheckHost[];
|
||||||
|
|||||||
@@ -205,3 +205,16 @@ export namespace IBitcoinApi {
|
|||||||
"utxo_size_inc": number;
|
"utxo_size_inc": number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestMempoolAcceptResult {
|
||||||
|
txid: string,
|
||||||
|
wtxid: string,
|
||||||
|
allowed?: boolean,
|
||||||
|
vsize?: number,
|
||||||
|
fees?: {
|
||||||
|
base: number,
|
||||||
|
"effective-feerate": number,
|
||||||
|
"effective-includes": string[],
|
||||||
|
},
|
||||||
|
['reject-reason']?: string,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import mempool from '../mempool';
|
import mempool from '../mempool';
|
||||||
@@ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
|
const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2);
|
||||||
|
const transactions: IEsploraApi.Transaction[] = [];
|
||||||
|
for (const tx of verboseBlock.tx) {
|
||||||
|
const converted = await this.$convertTransaction(tx, true);
|
||||||
|
transactions.push(converted);
|
||||||
|
}
|
||||||
|
return transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<Buffer> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
@@ -159,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
const mp = mempool.getMempool();
|
const mp = mempool.getMempool();
|
||||||
for (const tx in mp) {
|
for (const tx in mp) {
|
||||||
for (const vout of mp[tx].vout) {
|
for (const vout of mp[tx].vout) {
|
||||||
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
|
if (vout.scriptpubkey_address?.indexOf(prefix) === 0) {
|
||||||
found[vout.scriptpubkey_address] = '';
|
found[vout.scriptpubkey_address] = '';
|
||||||
if (Object.keys(found).length >= 10) {
|
if (Object.keys(found).length >= 10) {
|
||||||
return Object.keys(found);
|
return Object.keys(found);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const vin of mp[tx].vin) {
|
||||||
|
if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) {
|
||||||
|
found[vin.prevout?.scriptpubkey_address] = '';
|
||||||
|
if (Object.keys(found).length >= 10) {
|
||||||
|
return Object.keys(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(found);
|
return Object.keys(found);
|
||||||
}
|
}
|
||||||
@@ -174,6 +188,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
|
||||||
|
if (rawTransactions.length) {
|
||||||
|
return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||||
return {
|
return {
|
||||||
@@ -224,6 +246,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return outspends;
|
return outspends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
|
||||||
|
const txids = await this.$getTxIdsForBlock(blockhash);
|
||||||
|
return this.$getRawTransaction(txids[0]);
|
||||||
|
}
|
||||||
|
|
||||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||||
// 120 is the default block span in Core
|
// 120 is the default block span in Core
|
||||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||||
@@ -296,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
'witness_v1_taproot': 'v1_p2tr',
|
'witness_v1_taproot': 'v1_p2tr',
|
||||||
'nonstandard': 'nonstandard',
|
'nonstandard': 'nonstandard',
|
||||||
'multisig': 'multisig',
|
'multisig': 'multisig',
|
||||||
|
'anchor': 'anchor',
|
||||||
'nulldata': 'op_return'
|
'nulldata': 'op_return'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import bitcoinClient from './bitcoin-client';
|
|||||||
import difficultyAdjustment from '../difficulty-adjustment';
|
import difficultyAdjustment from '../difficulty-adjustment';
|
||||||
import transactionRepository from '../../repositories/TransactionRepository';
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
import rbfCache from '../rbf-cache';
|
import rbfCache from '../rbf-cache';
|
||||||
import { calculateCpfp } from '../cpfp';
|
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@@ -37,65 +38,12 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
|
||||||
responseType: 'stream', timeout: 10000
|
|
||||||
});
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
|
||||||
responseType: 'stream', timeout: 10000
|
|
||||||
});
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
|
||||||
responseType: 'stream', timeout: 10000
|
|
||||||
});
|
|
||||||
response.data.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).end();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||||
@@ -109,6 +57,7 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
|
||||||
|
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
||||||
@@ -138,7 +87,7 @@ class BitcoinRoutes {
|
|||||||
res.set('Content-Type', 'application/json');
|
res.set('Content-Type', 'application/json');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,13 +106,13 @@ class BitcoinRoutes {
|
|||||||
const result = mempoolBlocks.getMempoolBlocks();
|
const result = mempoolBlocks.getMempoolBlocks();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTransactionTimes(req: Request, res: Response) {
|
private getTransactionTimes(req: Request, res: Response) {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(500).send('Not an array');
|
handleError(req, res, 500, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@@ -180,12 +129,12 @@ class BitcoinRoutes {
|
|||||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||||
const txids_csv = req.query.txids;
|
const txids_csv = req.query.txids;
|
||||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||||
res.status(500).send('Invalid txids format');
|
handleError(req, res, 500, 'Invalid txids format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txids = txids_csv.split(',');
|
const txids = txids_csv.split(',');
|
||||||
if (txids.length > 50) {
|
if (txids.length > 50) {
|
||||||
res.status(400).send('Too many txids requested');
|
handleError(req, res, 400, 'Too many txids requested');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,13 +142,13 @@ class BitcoinRoutes {
|
|||||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||||
res.json(batchedOutspends);
|
res.json(batchedOutspends);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
handleError(req, res, 501, `Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,13 +161,17 @@ class BitcoinRoutes {
|
|||||||
descendants: tx.descendants || null,
|
descendants: tx.descendants || null,
|
||||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
sigops: tx.sigops,
|
sigops: tx.sigops,
|
||||||
|
fee: tx.fee,
|
||||||
adjustedVsize: tx.adjustedVsize,
|
adjustedVsize: tx.adjustedVsize,
|
||||||
acceleration: tx.acceleration
|
acceleration: tx.acceleration,
|
||||||
|
acceleratedBy: tx.acceleratedBy || undefined,
|
||||||
|
acceleratedAt: tx.acceleratedAt || undefined,
|
||||||
|
feeDelta: tx.feeDelta || undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cpfpInfo = calculateCpfp(tx, mempool.getMempool());
|
const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool());
|
||||||
|
|
||||||
res.json(cpfpInfo);
|
res.json(cpfpInfo);
|
||||||
return;
|
return;
|
||||||
@@ -228,7 +181,7 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send('failed to get CPFP info');
|
handleError(req, res, 500, 'failed to get CPFP info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +210,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +224,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,13 +285,13 @@ class BitcoinRoutes {
|
|||||||
// Not modified
|
// Not modified
|
||||||
// 422 Unprocessable Entity
|
// 422 Unprocessable Entity
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||||
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
|
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +305,7 @@ class BitcoinRoutes {
|
|||||||
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +315,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +337,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
||||||
res.json(block);
|
res.json(block);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +347,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(blockHeader);
|
res.send(blockHeader);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +358,23 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(auditSummary);
|
res.json(auditSummary);
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send(`audit not available`);
|
handleError(req, res, 404, `audit not available`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getBlockTxAuditSummary(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
|
||||||
|
if (auditSummary) {
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(auditSummary);
|
||||||
|
} else {
|
||||||
|
handleError(req, res, 404, `transaction audit not available`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
@@ -422,42 +391,49 @@ class BitcoinRoutes {
|
|||||||
return await this.getLegacyBlocks(req, res);
|
return await this.getLegacyBlocks(req, res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlocksByBulk(req: Request, res: Response) {
|
private async getBlocksByBulk(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
||||||
return res.status(404).send(`This API is only available for Bitcoin networks`);
|
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
||||||
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!Common.indexingEnabled()) {
|
if (!Common.indexingEnabled()) {
|
||||||
return res.status(404).send(`Indexing is required for this API`);
|
handleError(req, res, 404, `Indexing is required for this API`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = parseInt(req.params.from, 10);
|
const from = parseInt(req.params.from, 10);
|
||||||
if (!req.params.from || from < 0) {
|
if (!req.params.from || from < 0) {
|
||||||
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
||||||
if (to < 0) {
|
if (to < 0) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
|
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
||||||
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,10 +468,10 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(returnBlocks);
|
res.json(returnBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlockTransactions(req: Request, res: Response) {
|
private async getBlockTransactions(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||||
@@ -517,7 +493,7 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,13 +502,13 @@ class BitcoinRoutes {
|
|||||||
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddress(req: Request, res: Response) {
|
private async getAddress(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,15 +517,16 @@ class BitcoinRoutes {
|
|||||||
res.json(addressData);
|
res.json(addressData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,23 +539,23 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Address summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHash(req: Request, res: Response) {
|
private async getScriptHash(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,15 +566,16 @@ class BitcoinRoutes {
|
|||||||
res.json(addressData);
|
res.json(addressData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,16 +590,16 @@ class BitcoinRoutes {
|
|||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +609,7 @@ class BitcoinRoutes {
|
|||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +636,7 @@ class BitcoinRoutes {
|
|||||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||||
res.send(rawMempool);
|
res.send(rawMempool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,12 +644,13 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
const result = blocks.getCurrentBlockHeight();
|
const result = blocks.getCurrentBlockHeight();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result.toString());
|
res.send(result.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +660,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +670,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'application/octet-stream');
|
res.setHeader('content-type', 'application/octet-stream');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +679,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +688,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinClient.validateAddress(req.params.address);
|
const result = await bitcoinClient.validateAddress(req.params.address);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,7 +701,7 @@ class BitcoinRoutes {
|
|||||||
replaces
|
replaces
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,7 +710,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(false);
|
const result = rbfCache.getRbfTrees(false);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +719,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(true);
|
const result = rbfCache.getRbfTrees(true);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +732,7 @@ class BitcoinRoutes {
|
|||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,7 +741,7 @@ class BitcoinRoutes {
|
|||||||
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,10 +751,10 @@ class BitcoinRoutes {
|
|||||||
if (da) {
|
if (da) {
|
||||||
res.json(da);
|
res.json(da);
|
||||||
} else {
|
} else {
|
||||||
res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,7 +765,7 @@ class BitcoinRoutes {
|
|||||||
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,7 +777,19 @@ class BitcoinRoutes {
|
|||||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
|
: (e.message || 'Error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $testTransactions(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const rawTxs = Common.getTransactionsFromRequest(req);
|
||||||
|
const maxfeerate = parseFloat(req.query.maxfeerate as string);
|
||||||
|
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e: any) {
|
||||||
|
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export namespace IEsploraApi {
|
|||||||
scriptpubkey: string;
|
scriptpubkey: string;
|
||||||
scriptpubkey_asm: string;
|
scriptpubkey_asm: string;
|
||||||
scriptpubkey_type: string;
|
scriptpubkey_type: string;
|
||||||
scriptpubkey_address: string;
|
scriptpubkey_address?: string;
|
||||||
value: number;
|
value: number;
|
||||||
// Elements
|
// Elements
|
||||||
valuecommitment?: number;
|
valuecommitment?: number;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
|||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
|
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||||
|
|
||||||
interface FailoverHost {
|
interface FailoverHost {
|
||||||
host: string,
|
host: string,
|
||||||
@@ -24,6 +25,7 @@ interface FailoverHost {
|
|||||||
class FailoverRouter {
|
class FailoverRouter {
|
||||||
activeHost: FailoverHost;
|
activeHost: FailoverHost;
|
||||||
fallbackHost: FailoverHost;
|
fallbackHost: FailoverHost;
|
||||||
|
maxSlippage: number = config.ESPLORA.MAX_BEHIND_TIP ?? 2;
|
||||||
maxHeight: number = 0;
|
maxHeight: number = 0;
|
||||||
hosts: FailoverHost[];
|
hosts: FailoverHost[];
|
||||||
multihost: boolean;
|
multihost: boolean;
|
||||||
@@ -92,13 +94,13 @@ class FailoverRouter {
|
|||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
const height = result.data;
|
const height = result.data;
|
||||||
this.maxHeight = Math.max(height, this.maxHeight);
|
host.latestHeight = height;
|
||||||
|
this.maxHeight = Math.max(height || 0, ...this.hosts.map(h => (!(h.unreachable || h.timedOut || h.outOfSync) ? h.latestHeight || 0 : 0)));
|
||||||
const rtt = result.config['meta'].rtt;
|
const rtt = result.config['meta'].rtt;
|
||||||
host.rtts.unshift(rtt);
|
host.rtts.unshift(rtt);
|
||||||
host.rtts.slice(0, 5);
|
host.rtts.slice(0, 5);
|
||||||
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
||||||
host.latestHeight = height;
|
if (height == null || isNaN(height) || (this.maxHeight - height > this.maxSlippage)) {
|
||||||
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
|
|
||||||
host.outOfSync = true;
|
host.outOfSync = true;
|
||||||
} else {
|
} else {
|
||||||
host.outOfSync = false;
|
host.outOfSync = false;
|
||||||
@@ -125,7 +127,6 @@ class FailoverRouter {
|
|||||||
host.checked = true;
|
host.checked = true;
|
||||||
host.lastChecked = Date.now();
|
host.lastChecked = Date.now();
|
||||||
|
|
||||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
|
||||||
const rankOrder = this.sortHosts();
|
const rankOrder = this.sortHosts();
|
||||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
// switch if the current host is out of sync or significantly slower than the next best alternative
|
||||||
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
|
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
|
||||||
@@ -183,7 +184,6 @@ class FailoverRouter {
|
|||||||
|
|
||||||
// depose the active host and choose the next best replacement
|
// depose the active host and choose the next best replacement
|
||||||
private electHost(): void {
|
private electHost(): void {
|
||||||
this.activeHost.outOfSync = true;
|
|
||||||
this.activeHost.failures = 0;
|
this.activeHost.failures = 0;
|
||||||
const rankOrder = this.sortHosts();
|
const rankOrder = this.sortHosts();
|
||||||
this.activeHost = rankOrder[0];
|
this.activeHost = rankOrder[0];
|
||||||
@@ -194,6 +194,7 @@ class FailoverRouter {
|
|||||||
host.failures++;
|
host.failures++;
|
||||||
if (host.failures > 5 && this.multihost) {
|
if (host.failures > 5 && this.multihost) {
|
||||||
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
|
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
|
||||||
|
this.activeHost.unreachable = true;
|
||||||
this.electHost();
|
this.electHost();
|
||||||
return this.activeHost;
|
return this.activeHost;
|
||||||
} else {
|
} else {
|
||||||
@@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
||||||
}
|
}
|
||||||
@@ -347,6 +352,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
|
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
|
||||||
|
const txid = await this.failoverRouter.$get<string>(`/block/${blockhash}/txid/0`);
|
||||||
|
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
|
||||||
|
}
|
||||||
|
|
||||||
public startHealthChecks(): void {
|
public startHealthChecks(): void {
|
||||||
this.failoverRouter.startHealthChecks();
|
this.failoverRouter.startHealthChecks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import config from '../config';
|
|||||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
|
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
@@ -29,6 +29,11 @@ import websocketHandler from './websocket-handler';
|
|||||||
import redisCache from './redis-cache';
|
import redisCache from './redis-cache';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
import { calcBitsDifference } from './difficulty-adjustment';
|
import { calcBitsDifference } from './difficulty-adjustment';
|
||||||
|
import AccelerationRepository from '../repositories/AccelerationRepository';
|
||||||
|
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
||||||
|
import mempool from './mempool';
|
||||||
|
import CpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import accelerationApi from './services/acceleration';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@@ -214,10 +219,10 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||||
return {
|
return {
|
||||||
id: hash,
|
id: hash,
|
||||||
transactions: Common.classifyTransactions(transactions),
|
transactions: Common.classifyTransactions(transactions, height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,10 +299,12 @@ class Blocks {
|
|||||||
extras.virtualSize = block.weight / 4.0;
|
extras.virtualSize = block.weight / 4.0;
|
||||||
if (coinbaseTx?.vout.length > 0) {
|
if (coinbaseTx?.vout.length > 0) {
|
||||||
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
|
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
|
||||||
|
extras.coinbaseAddresses = [...new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[])];
|
||||||
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
|
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
|
||||||
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
|
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
|
||||||
} else {
|
} else {
|
||||||
extras.coinbaseAddress = null;
|
extras.coinbaseAddress = null;
|
||||||
|
extras.coinbaseAddresses = null;
|
||||||
extras.coinbaseSignature = null;
|
extras.coinbaseSignature = null;
|
||||||
extras.coinbaseSignatureAscii = null;
|
extras.coinbaseSignatureAscii = null;
|
||||||
}
|
}
|
||||||
@@ -369,8 +376,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
|
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter(address => address) as string[];
|
||||||
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
|
|
||||||
|
|
||||||
let pools: PoolTag[] = [];
|
let pools: PoolTag[] = [];
|
||||||
if (config.DATABASE.ENABLED === true) {
|
if (config.DATABASE.ENABLED === true) {
|
||||||
@@ -379,26 +385,9 @@ class Blocks {
|
|||||||
pools = poolsParser.miningPools;
|
pools = poolsParser.miningPools;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < pools.length; ++i) {
|
const pool = poolsParser.matchBlockMiner(txMinerInfo.vin[0].scriptsig, addresses || [], pools);
|
||||||
if (addresses.length) {
|
if (pool) {
|
||||||
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
|
return pool;
|
||||||
JSON.parse(pools[i].addresses) : pools[i].addresses;
|
|
||||||
for (let y = 0; y < poolAddresses.length; y++) {
|
|
||||||
if (addresses.indexOf(poolAddresses[y]) !== -1) {
|
|
||||||
return pools[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const regexes: string[] = typeof pools[i].regexes === 'string' ?
|
|
||||||
JSON.parse(pools[i].regexes) : pools[i].regexes;
|
|
||||||
for (let y = 0; y < regexes.length; ++y) {
|
|
||||||
const regex = new RegExp(regexes[y], 'i');
|
|
||||||
const match = asciiScriptSig.match(regex);
|
|
||||||
if (match !== null) {
|
|
||||||
return pools[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED === true) {
|
if (config.DATABASE.ENABLED === true) {
|
||||||
@@ -451,7 +440,7 @@ class Blocks {
|
|||||||
|
|
||||||
|
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||||
if (cpfpSummary) {
|
if (cpfpSummary) {
|
||||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||||
@@ -582,8 +571,11 @@ class Blocks {
|
|||||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
const currentBlockHeight = blockchainInfo.blocks;
|
const currentBlockHeight = blockchainInfo.blocks;
|
||||||
|
|
||||||
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0);
|
const targetSummaryVersion: number = 1;
|
||||||
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0);
|
const targetTemplateVersion: number = 1;
|
||||||
|
|
||||||
|
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion);
|
||||||
|
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion);
|
||||||
|
|
||||||
// nothing to do
|
// nothing to do
|
||||||
if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) {
|
if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) {
|
||||||
@@ -616,16 +608,24 @@ class Blocks {
|
|||||||
|
|
||||||
for (let height = currentBlockHeight; height >= 0; height--) {
|
for (let height = currentBlockHeight; height >= 0; height--) {
|
||||||
try {
|
try {
|
||||||
let txs: TransactionExtended[] | null = null;
|
let txs: MempoolTransactionExtended[] | null = null;
|
||||||
if (unclassifiedBlocks[height]) {
|
if (unclassifiedBlocks[height]) {
|
||||||
const blockHash = unclassifiedBlocks[height];
|
const blockHash = unclassifiedBlocks[height];
|
||||||
// fetch transactions
|
// fetch transactions
|
||||||
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || [];
|
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || [];
|
||||||
// add CPFP
|
// add CPFP
|
||||||
const cpfpSummary = Common.calculateCpfp(height, txs, true);
|
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
|
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||||
|
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||||
|
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||||
|
if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) {
|
||||||
|
// CPFP clusters changed - update the compact_cpfp tables
|
||||||
|
await CpfpRepository.$deleteClustersAt(height);
|
||||||
|
await this.$saveCpfp(blockHash, height, cpfpSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
await Common.sleep$(250);
|
await Common.sleep$(250);
|
||||||
}
|
}
|
||||||
if (unclassifiedTemplates[height]) {
|
if (unclassifiedTemplates[height]) {
|
||||||
@@ -651,9 +651,9 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
templateTxs.push(tx || templateTx);
|
templateTxs.push(tx || templateTx);
|
||||||
}
|
}
|
||||||
const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true);
|
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||||
for (const tx of classifiedTxs) {
|
for (const tx of classifiedTxs) {
|
||||||
classifiedTxMap[tx.txid] = tx;
|
classifiedTxMap[tx.txid] = tx;
|
||||||
@@ -689,6 +689,52 @@ class Blocks {
|
|||||||
this.classifyingBlocks = false;
|
this.classifyingBlocks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Index missing coinbase addresses for all blocks
|
||||||
|
*/
|
||||||
|
public async $indexCoinbaseAddresses(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get all indexed block hash
|
||||||
|
const unindexedBlocks = await blocksRepository.$getBlocksWithoutCoinbaseAddresses();
|
||||||
|
|
||||||
|
if (!unindexedBlocks?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Indexing missing coinbase addresses for ${unindexedBlocks.length} blocks`);
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let count = 0;
|
||||||
|
let countThisRun = 0;
|
||||||
|
let timer = Date.now() / 1000;
|
||||||
|
const startedAt = Date.now() / 1000;
|
||||||
|
for (const { height, hash } of unindexedBlocks) {
|
||||||
|
// Logging
|
||||||
|
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const runningFor = (Date.now() / 1000) - startedAt;
|
||||||
|
const blockPerSeconds = countThisRun / elapsedSeconds;
|
||||||
|
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
||||||
|
logger.debug(`Indexing coinbase addresses for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||||
|
timer = Date.now() / 1000;
|
||||||
|
countThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash);
|
||||||
|
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]);
|
||||||
|
await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]);
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
count++;
|
||||||
|
countThisRun++;
|
||||||
|
}
|
||||||
|
logger.notice(`coinbase addresses indexing completed: indexed ${count} blocks`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`coinbase addresses indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
@@ -838,8 +884,11 @@ class Blocks {
|
|||||||
} else {
|
} else {
|
||||||
this.currentBlockHeight++;
|
this.currentBlockHeight++;
|
||||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||||
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
// skip updating the orphan block cache if we've fallen behind the chain tip
|
||||||
await chainTips.updateOrphanedBlocks();
|
if (this.currentBlockHeight >= blockHeightTip - 2) {
|
||||||
|
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||||
|
await chainTips.updateOrphanedBlocks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
||||||
@@ -856,9 +905,14 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
let accelerations = Object.values(mempool.getAccelerations());
|
||||||
|
if (accelerations?.length > 0) {
|
||||||
|
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
|
||||||
|
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
|
||||||
|
}
|
||||||
|
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@@ -872,18 +926,19 @@ class Blocks {
|
|||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
||||||
|
await AccelerationRepository.$deleteAccelerationsFrom(lastBlock.height - 10);
|
||||||
this.blocks = this.blocks.slice(0, -10);
|
this.blocks = this.blocks.slice(0, -10);
|
||||||
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
|
||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||||
this.blocks.push(newBlock);
|
this.blocks.push(newBlock);
|
||||||
this.updateTimerProgress(timer, `reindexed block`);
|
this.updateTimerProgress(timer, `reindexed block`);
|
||||||
let cpfpSummary;
|
let newCpfpSummary;
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||||
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||||
}
|
}
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
|
||||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
@@ -932,7 +987,7 @@ class Blocks {
|
|||||||
|
|
||||||
// start async callbacks
|
// start async callbacks
|
||||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@@ -974,6 +1029,9 @@ class Blocks {
|
|||||||
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
}
|
}
|
||||||
|
blockSummary.transactions.forEach(tx => {
|
||||||
|
delete tx.acc;
|
||||||
|
});
|
||||||
this.blockSummaries.push(blockSummary);
|
this.blockSummaries.push(blockSummary);
|
||||||
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
@@ -1111,12 +1169,13 @@ class Blocks {
|
|||||||
transactions: cpfpSummary.transactions.map(tx => {
|
transactions: cpfpSummary.transactions.map(tx => {
|
||||||
let flags: number = 0;
|
let flags: number = 0;
|
||||||
try {
|
try {
|
||||||
flags = tx.flags || Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
|
time: tx.firstSeen,
|
||||||
fee: tx.fee || 0,
|
fee: tx.fee || 0,
|
||||||
vsize: tx.vsize,
|
vsize: tx.vsize,
|
||||||
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
|
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
|
||||||
@@ -1125,11 +1184,11 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
summaryVersion = 1;
|
summaryVersion = cpfpSummary.version;
|
||||||
} else {
|
} else {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(hash, txs);
|
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@@ -1250,6 +1309,7 @@ class Blocks {
|
|||||||
utxoset_size: block.extras.utxoSetSize ?? null,
|
utxoset_size: block.extras.utxoSetSize ?? null,
|
||||||
coinbase_raw: block.extras.coinbaseRaw ?? null,
|
coinbase_raw: block.extras.coinbaseRaw ?? null,
|
||||||
coinbase_address: block.extras.coinbaseAddress ?? null,
|
coinbase_address: block.extras.coinbaseAddress ?? null,
|
||||||
|
coinbase_addresses: block.extras.coinbaseAddresses ?? null,
|
||||||
coinbase_signature: block.extras.coinbaseSignature ?? null,
|
coinbase_signature: block.extras.coinbaseSignature ?? null,
|
||||||
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
|
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
|
||||||
pool_slug: block.extras.pool.slug ?? null,
|
pool_slug: block.extras.pool.slug ?? null,
|
||||||
@@ -1264,7 +1324,7 @@ class Blocks {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@@ -1319,6 +1379,14 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
|
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getLastDifficultyAdjustmentTime(): number {
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
return this.lastDifficultyAdjustmentTime;
|
return this.lastDifficultyAdjustmentTime;
|
||||||
}
|
}
|
||||||
@@ -1335,11 +1403,11 @@ class Blocks {
|
|||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
|
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
|
||||||
let transactions = txs;
|
let transactions = txs;
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||||
}
|
}
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
@@ -1351,7 +1419,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (transactions?.length != null) {
|
if (transactions?.length != null) {
|
||||||
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
|
const summary = calculateFastBlockCpfp(height, transactions);
|
||||||
|
|
||||||
await this.$saveCpfp(hash, height, summary);
|
await this.$saveCpfp(hash, height, summary);
|
||||||
|
|
||||||
|
|||||||
@@ -12,32 +12,68 @@ export interface OrphanedBlock {
|
|||||||
height: number;
|
height: number;
|
||||||
hash: string;
|
hash: string;
|
||||||
status: 'valid-fork' | 'valid-headers' | 'headers-only';
|
status: 'valid-fork' | 'valid-headers' | 'headers-only';
|
||||||
|
prevhash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChainTips {
|
class ChainTips {
|
||||||
private chainTips: ChainTip[] = [];
|
private chainTips: ChainTip[] = [];
|
||||||
private orphanedBlocks: OrphanedBlock[] = [];
|
private orphanedBlocks: { [hash: string]: OrphanedBlock } = {};
|
||||||
|
private blockCache: { [hash: string]: OrphanedBlock } = {};
|
||||||
|
private orphansByHeight: { [height: number]: OrphanedBlock[] } = {};
|
||||||
|
|
||||||
public async updateOrphanedBlocks(): Promise<void> {
|
public async updateOrphanedBlocks(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.chainTips = await bitcoinClient.getChainTips();
|
this.chainTips = await bitcoinClient.getChainTips();
|
||||||
this.orphanedBlocks = [];
|
|
||||||
|
const start = Date.now();
|
||||||
|
const breakAt = start + 10000;
|
||||||
|
let newOrphans = 0;
|
||||||
|
this.orphanedBlocks = {};
|
||||||
|
|
||||||
for (const chain of this.chainTips) {
|
for (const chain of this.chainTips) {
|
||||||
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
|
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
|
||||||
let block = await bitcoinClient.getBlock(chain.hash);
|
const orphans: OrphanedBlock[] = [];
|
||||||
while (block && block.confirmations === -1) {
|
let hash = chain.hash;
|
||||||
this.orphanedBlocks.push({
|
do {
|
||||||
height: block.height,
|
let orphan = this.blockCache[hash];
|
||||||
hash: block.hash,
|
if (!orphan) {
|
||||||
status: chain.status
|
const block = await bitcoinClient.getBlock(hash);
|
||||||
});
|
if (block && block.confirmations === -1) {
|
||||||
block = await bitcoinClient.getBlock(block.previousblockhash);
|
newOrphans++;
|
||||||
|
orphan = {
|
||||||
|
height: block.height,
|
||||||
|
hash: block.hash,
|
||||||
|
status: chain.status,
|
||||||
|
prevhash: block.previousblockhash,
|
||||||
|
};
|
||||||
|
this.blockCache[hash] = orphan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (orphan) {
|
||||||
|
orphans.push(orphan);
|
||||||
|
}
|
||||||
|
hash = orphan?.prevhash;
|
||||||
|
} while (hash && (Date.now() < breakAt));
|
||||||
|
for (const orphan of orphans) {
|
||||||
|
this.orphanedBlocks[orphan.hash] = orphan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Date.now() >= breakAt) {
|
||||||
|
logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`);
|
this.orphansByHeight = {};
|
||||||
|
const allOrphans = Object.values(this.orphanedBlocks);
|
||||||
|
for (const orphan of allOrphans) {
|
||||||
|
if (!this.orphansByHeight[orphan.height]) {
|
||||||
|
this.orphansByHeight[orphan.height] = [];
|
||||||
|
}
|
||||||
|
this.orphansByHeight[orphan.height].push(orphan);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
|
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
}
|
}
|
||||||
@@ -48,13 +84,7 @@ class ChainTips {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const orphans: OrphanedBlock[] = [];
|
return this.orphansByHeight[height] || [];
|
||||||
for (const block of this.orphanedBlocks) {
|
|
||||||
if (block.height === height) {
|
|
||||||
orphans.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orphans;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
@@ -10,7 +10,6 @@ import logger from '../logger';
|
|||||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
// Bitcoin Core default policy settings
|
// Bitcoin Core default policy settings
|
||||||
const TX_MAX_STANDARD_VERSION = 2;
|
|
||||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||||
@@ -80,8 +79,8 @@ export class Common {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||||
|
|
||||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||||
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
||||||
@@ -96,7 +95,7 @@ export class Common {
|
|||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
});
|
});
|
||||||
if (foundMatches?.length) {
|
if (foundMatches?.length) {
|
||||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -124,7 +123,7 @@ export class Common {
|
|||||||
foundMatches.add(deletedTx);
|
foundMatches.add(deletedTx);
|
||||||
}
|
}
|
||||||
if (foundMatches.size) {
|
if (foundMatches.size) {
|
||||||
matches[addedTx.txid] = [...foundMatches];
|
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,17 +138,17 @@ export class Common {
|
|||||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
const vin = tx.vin[i];
|
const vin = tx.vin[i];
|
||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const key = `${vin.txid}:${vin.vout}`;
|
||||||
|
const match = spendMap.get(key);
|
||||||
if (match && match.txid !== tx.txid) {
|
if (match && match.txid !== tx.txid) {
|
||||||
replaced.add(match);
|
replaced.add(match);
|
||||||
// remove this tx from the spendMap
|
// remove this tx from the spendMap
|
||||||
// prevents the same tx being replaced more than once
|
// prevents the same tx being replaced more than once
|
||||||
for (const replacedVin of match.vin) {
|
for (const replacedVin of match.vin) {
|
||||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||||
spendMap.delete(key);
|
spendMap.delete(replacedKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = `${vin.txid}:${vin.vout}`;
|
|
||||||
spendMap.delete(key);
|
spendMap.delete(key);
|
||||||
}
|
}
|
||||||
if (replaced.size) {
|
if (replaced.size) {
|
||||||
@@ -200,10 +199,13 @@ export class Common {
|
|||||||
*
|
*
|
||||||
* returns true early if any standardness rule is violated, otherwise false
|
* returns true early if any standardness rule is violated, otherwise false
|
||||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||||
|
*
|
||||||
|
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||||
|
* For now, just pull out individual rules into versioned functions where necessary.
|
||||||
*/
|
*/
|
||||||
static isNonStandard(tx: TransactionExtended): boolean {
|
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||||
// version
|
// version
|
||||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
if (this.isNonStandardVersion(tx, height)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +252,8 @@ export class Common {
|
|||||||
}
|
}
|
||||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// TODO: bad-witness-nonstandard
|
// TODO: bad-witness-nonstandard
|
||||||
}
|
}
|
||||||
@@ -258,9 +262,15 @@ export class Common {
|
|||||||
let opreturnCount = 0;
|
let opreturnCount = 0;
|
||||||
for (const vout of tx.vout) {
|
for (const vout of tx.vout) {
|
||||||
// scriptpubkey
|
// scriptpubkey
|
||||||
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||||
// (non-standard output type)
|
// (non-standard output type)
|
||||||
return true;
|
return true;
|
||||||
|
} else if (vout.scriptpubkey_type === 'unknown') {
|
||||||
|
// undefined segwit version/length combinations are actually standard in outputs
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
|
||||||
|
if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} else if (vout.scriptpubkey_type === 'multisig') {
|
} else if (vout.scriptpubkey_type === 'multisig') {
|
||||||
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
||||||
// bare-multisig
|
// bare-multisig
|
||||||
@@ -286,7 +296,7 @@ export class Common {
|
|||||||
dustSize += getVarIntLength(dustSize);
|
dustSize += getVarIntLength(dustSize);
|
||||||
// add value size
|
// add value size
|
||||||
dustSize += 8;
|
dustSize += 8;
|
||||||
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
|
if (Common.isWitnessProgram(vout.scriptpubkey)) {
|
||||||
dustSize += 67;
|
dustSize += 67;
|
||||||
} else {
|
} else {
|
||||||
dustSize += 148;
|
dustSize += 148;
|
||||||
@@ -308,6 +318,70 @@ export class Common {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
|
||||||
|
// followed by a data push between 2 and 40 bytes.
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
|
||||||
|
static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
|
||||||
|
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const version = parseInt(scriptpubkey.slice(0,2), 16);
|
||||||
|
if (version !== 0 && version < 0x51 || version > 0x60) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const push = parseInt(scriptpubkey.slice(2,4), 16);
|
||||||
|
if (push + 2 === (scriptpubkey.length / 2)) {
|
||||||
|
return {
|
||||||
|
version: version ? version - 0x50 : 0,
|
||||||
|
program: scriptpubkey.slice(4),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual versioned standardness rules
|
||||||
|
|
||||||
|
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
let TX_MAX_STANDARD_VERSION = 3;
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
TX_MAX_STANDARD_VERSION = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||||
let weight = tx.weight;
|
let weight = tx.weight;
|
||||||
let hasWitness = false;
|
let hasWitness = false;
|
||||||
@@ -373,16 +447,34 @@ export class Common {
|
|||||||
].includes(pubkey);
|
].includes(pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getTransactionFlags(tx: TransactionExtended): number {
|
static isInscription(vin, flags): bigint {
|
||||||
|
// in taproot, if the last witness item begins with 0x50, it's an annex
|
||||||
|
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
||||||
|
// script spends have more than one witness item, not counting the annex (if present)
|
||||||
|
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
||||||
|
// the script itself is the second-to-last witness item, not counting the annex
|
||||||
|
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
|
||||||
|
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
||||||
|
if (asm?.includes('OP_0 OP_IF')) {
|
||||||
|
flags |= TransactionFlags.inscription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||||
|
|
||||||
// Update variable flags (CPFP, RBF)
|
// Update variable flags (CPFP, RBF)
|
||||||
|
flags &= ~TransactionFlags.cpfp_child;
|
||||||
if (tx.ancestors?.length) {
|
if (tx.ancestors?.length) {
|
||||||
flags |= TransactionFlags.cpfp_child;
|
flags |= TransactionFlags.cpfp_child;
|
||||||
}
|
}
|
||||||
|
flags &= ~TransactionFlags.cpfp_parent;
|
||||||
if (tx.descendants?.length) {
|
if (tx.descendants?.length) {
|
||||||
flags |= TransactionFlags.cpfp_parent;
|
flags |= TransactionFlags.cpfp_parent;
|
||||||
}
|
}
|
||||||
|
flags &= ~TransactionFlags.replacement;
|
||||||
if (tx.replacement) {
|
if (tx.replacement) {
|
||||||
flags |= TransactionFlags.replacement;
|
flags |= TransactionFlags.replacement;
|
||||||
}
|
}
|
||||||
@@ -409,30 +501,30 @@ export class Common {
|
|||||||
if (vin.sequence < 0xfffffffe) {
|
if (vin.sequence < 0xfffffffe) {
|
||||||
rbf = true;
|
rbf = true;
|
||||||
}
|
}
|
||||||
switch (vin.prevout?.scriptpubkey_type) {
|
if (vin.prevout?.scriptpubkey_type) {
|
||||||
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
switch (vin.prevout?.scriptpubkey_type) {
|
||||||
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
||||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
||||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||||
case 'v1_p2tr': {
|
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||||
if (!vin.witness?.length) {
|
case 'v1_p2tr': {
|
||||||
throw new Error('Taproot input missing witness data');
|
flags |= TransactionFlags.p2tr;
|
||||||
}
|
if (vin.witness?.length) {
|
||||||
flags |= TransactionFlags.p2tr;
|
flags = Common.isInscription(vin, flags);
|
||||||
// in taproot, if the last witness item begins with 0x50, it's an annex
|
|
||||||
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
|
||||||
// script spends have more than one witness item, not counting the annex (if present)
|
|
||||||
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
|
||||||
// the script itself is the second-to-last witness item, not counting the annex
|
|
||||||
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
|
|
||||||
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
|
||||||
if (asm?.includes('OP_0 OP_IF')) {
|
|
||||||
flags |= TransactionFlags.inscription;
|
|
||||||
}
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no prevouts, optimistically check witness-bearing inputs
|
||||||
|
if (vin.witness?.length >= 2) {
|
||||||
|
try {
|
||||||
|
flags = Common.isInscription(vin, flags);
|
||||||
|
} catch {
|
||||||
|
// witness script parsing will fail if this isn't really a taproot output
|
||||||
}
|
}
|
||||||
} break;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sighash flags
|
// sighash flags
|
||||||
@@ -503,7 +595,7 @@ export class Common {
|
|||||||
if (hasFakePubkey) {
|
if (hasFakePubkey) {
|
||||||
flags |= TransactionFlags.fake_pubkey;
|
flags |= TransactionFlags.fake_pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fast but bad heuristic to detect possible coinjoins
|
// fast but bad heuristic to detect possible coinjoins
|
||||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||||
@@ -519,17 +611,17 @@ export class Common {
|
|||||||
flags |= TransactionFlags.batch_payout;
|
flags |= TransactionFlags.batch_payout;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isNonStandard(tx)) {
|
if (this.isNonStandard(tx, height)) {
|
||||||
flags |= TransactionFlags.nonstandard;
|
flags |= TransactionFlags.nonstandard;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(flags);
|
return Number(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
try {
|
try {
|
||||||
flags = Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@@ -540,8 +632,8 @@ export class Common {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||||
return txs.map(Common.classifyTransaction);
|
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||||
@@ -764,96 +856,6 @@ export class Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
|
||||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
|
||||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
|
||||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
|
||||||
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
|
|
||||||
const txMap: { [txid: string]: TransactionExtended } = {};
|
|
||||||
// initialize the txMap
|
|
||||||
for (const tx of transactions) {
|
|
||||||
txMap[tx.txid] = tx;
|
|
||||||
}
|
|
||||||
// reverse pass to identify CPFP clusters
|
|
||||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
|
||||||
const tx = transactions[i];
|
|
||||||
if (!ancestors[tx.txid]) {
|
|
||||||
let totalFee = 0;
|
|
||||||
let totalVSize = 0;
|
|
||||||
clusterTxs.forEach(tx => {
|
|
||||||
totalFee += tx?.fee || 0;
|
|
||||||
totalVSize += (tx.weight / 4);
|
|
||||||
});
|
|
||||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
|
||||||
let cluster: CpfpCluster;
|
|
||||||
if (clusterTxs.length > 1) {
|
|
||||||
cluster = {
|
|
||||||
root: clusterTxs[0].txid,
|
|
||||||
height,
|
|
||||||
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
|
||||||
effectiveFeePerVsize,
|
|
||||||
};
|
|
||||||
clusters.push(cluster);
|
|
||||||
}
|
|
||||||
clusterTxs.forEach(tx => {
|
|
||||||
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
|
||||||
if (cluster) {
|
|
||||||
clusterMap[tx.txid] = cluster;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// reset working vars
|
|
||||||
clusterTxs = [];
|
|
||||||
ancestors = {};
|
|
||||||
}
|
|
||||||
clusterTxs.push(tx);
|
|
||||||
tx.vin.forEach(vin => {
|
|
||||||
ancestors[vin.txid] = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// forward pass to enforce ancestor rate caps
|
|
||||||
for (const tx of transactions) {
|
|
||||||
let minAncestorRate = tx.effectiveFeePerVsize;
|
|
||||||
for (const vin of tx.vin) {
|
|
||||||
if (txMap[vin.txid]?.effectiveFeePerVsize) {
|
|
||||||
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check rounded values to skip cases with almost identical fees
|
|
||||||
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
|
|
||||||
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
|
|
||||||
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
|
|
||||||
tx.effectiveFeePerVsize = minAncestorRate;
|
|
||||||
if (!clusterMap[tx.txid]) {
|
|
||||||
// add a single-tx cluster to record the dependent rate
|
|
||||||
const cluster = {
|
|
||||||
root: tx.txid,
|
|
||||||
height,
|
|
||||||
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
|
|
||||||
effectiveFeePerVsize: minAncestorRate,
|
|
||||||
};
|
|
||||||
clusterMap[tx.txid] = cluster;
|
|
||||||
clusters.push(cluster);
|
|
||||||
} else {
|
|
||||||
// update the existing cluster with the dependent rate
|
|
||||||
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (saveRelatives) {
|
|
||||||
for (const cluster of clusters) {
|
|
||||||
cluster.txs.forEach((member, index) => {
|
|
||||||
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
|
|
||||||
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
|
|
||||||
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
transactions,
|
|
||||||
clusters,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
|
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
|
||||||
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
|
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
|
||||||
|
|
||||||
@@ -861,9 +863,10 @@ export class Common {
|
|||||||
let medianFee = 0;
|
let medianFee = 0;
|
||||||
let medianWeight = 0;
|
let medianWeight = 0;
|
||||||
|
|
||||||
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
|
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
|
||||||
const leftBound = 1995000;
|
const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800;
|
||||||
const rightBound = 2005000;
|
const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth);
|
||||||
|
const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth);
|
||||||
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
|
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
|
||||||
const left = weightCount;
|
const left = weightCount;
|
||||||
const right = weightCount + sortedTxs[i].weight;
|
const right = weightCount + sortedTxs[i].weight;
|
||||||
@@ -930,6 +933,33 @@ export class Common {
|
|||||||
return this.validateTransactionHex(matches[1].toLowerCase());
|
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTransactionsFromRequest(req: Request, limit: number = 25): string[] {
|
||||||
|
if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) {
|
||||||
|
throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit && req.body.length > limit) {
|
||||||
|
throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const txs = req.body;
|
||||||
|
|
||||||
|
return txs.map(rawTx => {
|
||||||
|
// Support both upper and lower case hex
|
||||||
|
// Support both txHash= Form and direct API POST
|
||||||
|
const reg = /^((?:[a-fA-F0-9]{2})+)$/;
|
||||||
|
const matches = reg.exec(rawTx);
|
||||||
|
if (!matches || !matches[1]) {
|
||||||
|
throw Object.assign(new Error('Invalid hex string'), { code: -2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to be a hex string of multiple of 2
|
||||||
|
// Guaranteed to be lower case
|
||||||
|
// Guaranteed to pass validation (see function below)
|
||||||
|
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static validateTransactionHex(txhex: string): string {
|
private static validateTransactionHex(txhex: string): string {
|
||||||
// Do not mutate txhex
|
// Do not mutate txhex
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,174 @@
|
|||||||
import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces';
|
import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
|
||||||
|
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
|
import { Acceleration } from './acceleration/acceleration';
|
||||||
|
|
||||||
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
||||||
const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
|
const MAX_CLUSTER_ITERATIONS = 100;
|
||||||
|
|
||||||
interface GraphTx extends MempoolTransactionExtended {
|
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||||
depends: string[];
|
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||||
spentby: string[];
|
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||||
ancestorMap: Map<string, GraphTx>;
|
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||||
fees: {
|
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
|
||||||
base: number;
|
const txMap: { [txid: string]: TransactionExtended } = {};
|
||||||
ancestor: number;
|
// initialize the txMap
|
||||||
|
for (const tx of transactions) {
|
||||||
|
txMap[tx.txid] = tx;
|
||||||
|
}
|
||||||
|
// reverse pass to identify CPFP clusters
|
||||||
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
|
const tx = transactions[i];
|
||||||
|
if (!ancestors[tx.txid]) {
|
||||||
|
let totalFee = 0;
|
||||||
|
let totalVSize = 0;
|
||||||
|
clusterTxs.forEach(tx => {
|
||||||
|
totalFee += tx?.fee || 0;
|
||||||
|
totalVSize += (tx.weight / 4);
|
||||||
|
});
|
||||||
|
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||||
|
let cluster: CpfpCluster;
|
||||||
|
if (clusterTxs.length > 1) {
|
||||||
|
cluster = {
|
||||||
|
root: clusterTxs[0].txid,
|
||||||
|
height,
|
||||||
|
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
||||||
|
effectiveFeePerVsize,
|
||||||
|
};
|
||||||
|
clusters.push(cluster);
|
||||||
|
}
|
||||||
|
clusterTxs.forEach(tx => {
|
||||||
|
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||||
|
if (cluster) {
|
||||||
|
clusterMap[tx.txid] = cluster;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// reset working vars
|
||||||
|
clusterTxs = [];
|
||||||
|
ancestors = {};
|
||||||
|
}
|
||||||
|
clusterTxs.push(tx);
|
||||||
|
tx.vin.forEach(vin => {
|
||||||
|
ancestors[vin.txid] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// forward pass to enforce ancestor rate caps
|
||||||
|
for (const tx of transactions) {
|
||||||
|
let minAncestorRate = tx.effectiveFeePerVsize;
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
if (txMap[vin.txid]?.effectiveFeePerVsize) {
|
||||||
|
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check rounded values to skip cases with almost identical fees
|
||||||
|
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
|
||||||
|
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
|
||||||
|
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
|
||||||
|
tx.effectiveFeePerVsize = minAncestorRate;
|
||||||
|
if (!clusterMap[tx.txid]) {
|
||||||
|
// add a single-tx cluster to record the dependent rate
|
||||||
|
const cluster = {
|
||||||
|
root: tx.txid,
|
||||||
|
height,
|
||||||
|
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
|
||||||
|
effectiveFeePerVsize: minAncestorRate,
|
||||||
|
};
|
||||||
|
clusterMap[tx.txid] = cluster;
|
||||||
|
clusters.push(cluster);
|
||||||
|
} else {
|
||||||
|
// update the existing cluster with the dependent rate
|
||||||
|
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saveRelatives) {
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
cluster.txs.forEach((member, index) => {
|
||||||
|
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
|
||||||
|
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
|
||||||
|
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
transactions,
|
||||||
|
clusters,
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary {
|
||||||
|
const txMap: { [txid: string]: MempoolTransactionExtended } = {};
|
||||||
|
for (const tx of transactions) {
|
||||||
|
txMap[tx.txid] = tx;
|
||||||
|
}
|
||||||
|
const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity);
|
||||||
|
const clusters = new Map<string, string[]>();
|
||||||
|
for (const tx of template) {
|
||||||
|
const cluster = tx.cluster || [];
|
||||||
|
const root = cluster.length ? cluster[cluster.length - 1] : null;
|
||||||
|
if (cluster.length > 1 && root && !clusters.has(root)) {
|
||||||
|
clusters.set(root, cluster);
|
||||||
|
}
|
||||||
|
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clusterArray: CpfpCluster[] = [];
|
||||||
|
|
||||||
|
for (const cluster of clusters.values()) {
|
||||||
|
for (const txid of cluster) {
|
||||||
|
const mempoolTx = txMap[txid];
|
||||||
|
if (mempoolTx) {
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
let matched = false;
|
||||||
|
cluster.forEach(relativeTxid => {
|
||||||
|
if (relativeTxid === txid) {
|
||||||
|
matched = true;
|
||||||
|
} else {
|
||||||
|
const relative = {
|
||||||
|
txid: relativeTxid,
|
||||||
|
fee: txMap[relativeTxid].fee,
|
||||||
|
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
|
||||||
|
};
|
||||||
|
if (matched) {
|
||||||
|
descendants.push(relative);
|
||||||
|
} else {
|
||||||
|
ancestors.push(relative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||||
|
mempoolTx.cpfpDirty = true;
|
||||||
|
}
|
||||||
|
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = cluster[cluster.length - 1];
|
||||||
|
clusterArray.push({
|
||||||
|
root: root,
|
||||||
|
height,
|
||||||
|
txs: cluster.reverse().map(txid => ({
|
||||||
|
txid,
|
||||||
|
fee: txMap[txid].fee,
|
||||||
|
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
|
||||||
|
})),
|
||||||
|
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||||
|
clusters: clusterArray,
|
||||||
|
version: 2,
|
||||||
};
|
};
|
||||||
ancestorcount: number;
|
|
||||||
ancestorsize: number;
|
|
||||||
ancestorRate: number;
|
|
||||||
individualRate: number;
|
|
||||||
score: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
|
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
|
||||||
* that transaction (and all others in the same cluster)
|
* that transaction (and all others in the same cluster)
|
||||||
*/
|
*/
|
||||||
export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||||
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
||||||
tx.cpfpDirty = false;
|
tx.cpfpDirty = false;
|
||||||
return {
|
return {
|
||||||
@@ -32,30 +177,31 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
|
|||||||
descendants: tx.descendants || [],
|
descendants: tx.descendants || [],
|
||||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||||
sigops: tx.sigops,
|
sigops: tx.sigops,
|
||||||
|
fee: tx.fee,
|
||||||
adjustedVsize: tx.adjustedVsize,
|
adjustedVsize: tx.adjustedVsize,
|
||||||
acceleration: tx.acceleration
|
acceleration: tx.acceleration
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ancestorMap = new Map<string, GraphTx>();
|
const ancestorMap = new Map<string, GraphTx>();
|
||||||
const graphTx = mempoolToGraphTx(tx);
|
const graphTx = convertToGraphTx(tx, memPool.getSpendMap());
|
||||||
ancestorMap.set(tx.txid, graphTx);
|
ancestorMap.set(tx.txid, graphTx);
|
||||||
|
|
||||||
const allRelatives = expandRelativesGraph(mempool, ancestorMap);
|
const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap());
|
||||||
const relativesMap = initializeRelatives(allRelatives);
|
const relativesMap = initializeRelatives(allRelatives);
|
||||||
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
|
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
|
||||||
|
|
||||||
let totalVsize = 0;
|
let totalVsize = 0;
|
||||||
let totalFee = 0;
|
let totalFee = 0;
|
||||||
for (const tx of cluster.values()) {
|
for (const tx of cluster.values()) {
|
||||||
totalVsize += tx.adjustedVsize;
|
totalVsize += tx.vsize;
|
||||||
totalFee += tx.fee;
|
totalFee += tx.fees.base;
|
||||||
}
|
}
|
||||||
const effectiveFeePerVsize = totalFee / totalVsize;
|
const effectiveFeePerVsize = totalFee / totalVsize;
|
||||||
for (const tx of cluster.values()) {
|
for (const tx of cluster.values()) {
|
||||||
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||||
mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||||
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||||
mempool[tx.txid].bestDescendant = null;
|
mempool[tx.txid].bestDescendant = null;
|
||||||
mempool[tx.txid].cpfpChecked = true;
|
mempool[tx.txid].cpfpChecked = true;
|
||||||
mempool[tx.txid].cpfpDirty = true;
|
mempool[tx.txid].cpfpDirty = true;
|
||||||
@@ -70,88 +216,12 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
|
|||||||
descendants: tx.descendants || [],
|
descendants: tx.descendants || [],
|
||||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||||
sigops: tx.sigops,
|
sigops: tx.sigops,
|
||||||
|
fee: tx.fee,
|
||||||
adjustedVsize: tx.adjustedVsize,
|
adjustedVsize: tx.adjustedVsize,
|
||||||
acceleration: tx.acceleration
|
acceleration: tx.acceleration
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
|
||||||
return {
|
|
||||||
...tx,
|
|
||||||
depends: tx.vin.map(v => v.txid),
|
|
||||||
spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[],
|
|
||||||
ancestorMap: new Map(),
|
|
||||||
fees: {
|
|
||||||
base: tx.fee,
|
|
||||||
ancestor: tx.fee,
|
|
||||||
},
|
|
||||||
ancestorcount: 1,
|
|
||||||
ancestorsize: tx.adjustedVsize,
|
|
||||||
ancestorRate: 0,
|
|
||||||
individualRate: 0,
|
|
||||||
score: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
|
||||||
*/
|
|
||||||
function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> {
|
|
||||||
const relatives: Map<string, GraphTx> = new Map();
|
|
||||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
|
||||||
while (stack.length > 0) {
|
|
||||||
if (relatives.size > MAX_GRAPH_SIZE) {
|
|
||||||
return relatives;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextTx = stack.pop();
|
|
||||||
if (!nextTx) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
relatives.set(nextTx.txid, nextTx);
|
|
||||||
|
|
||||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
|
||||||
if (relatives.has(relativeTxid)) {
|
|
||||||
// already processed this tx
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mempoolTx = ancestors.get(relativeTxid);
|
|
||||||
if (!mempoolTx && mempool[relativeTxid]) {
|
|
||||||
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
|
|
||||||
}
|
|
||||||
if (mempoolTx) {
|
|
||||||
stack.push(mempoolTx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return relatives;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
|
||||||
* by running setAncestors on each leaf, and caching intermediate results.
|
|
||||||
* then initializes ancestor data for each transaction
|
|
||||||
*
|
|
||||||
* @param all
|
|
||||||
*/
|
|
||||||
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
|
||||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
|
||||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
|
||||||
for (const leaf of leaves) {
|
|
||||||
setAncestors(leaf, mempoolTxs, visited);
|
|
||||||
}
|
|
||||||
mempoolTxs.forEach(entry => {
|
|
||||||
entry.ancestorMap?.forEach(ancestor => {
|
|
||||||
entry.ancestorcount++;
|
|
||||||
entry.ancestorsize += ancestor.adjustedVsize;
|
|
||||||
entry.fees.ancestor += ancestor.fees.base;
|
|
||||||
});
|
|
||||||
setAncestorScores(entry);
|
|
||||||
});
|
|
||||||
return mempoolTxs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a root transaction and a list of in-mempool ancestors,
|
* Given a root transaction and a list of in-mempool ancestors,
|
||||||
* Calculate the CPFP cluster
|
* Calculate the CPFP cluster
|
||||||
@@ -172,10 +242,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
|||||||
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
||||||
|
|
||||||
// Iterate until we reach a cluster that includes our target tx
|
// Iterate until we reach a cluster that includes our target tx
|
||||||
let maxIterations = MAX_GRAPH_SIZE;
|
let maxIterations = MAX_CLUSTER_ITERATIONS;
|
||||||
let best = sortedRelatives.shift();
|
let best = sortedRelatives.shift();
|
||||||
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
||||||
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
|
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) {
|
||||||
maxIterations--;
|
maxIterations--;
|
||||||
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
|
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
|
||||||
break;
|
break;
|
||||||
@@ -190,7 +260,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
|||||||
// Grab the next highest scoring entry
|
// Grab the next highest scoring entry
|
||||||
best = sortedRelatives.shift();
|
best = sortedRelatives.shift();
|
||||||
if (best) {
|
if (best) {
|
||||||
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
||||||
bestCluster.set(best?.txid, best);
|
bestCluster.set(best?.txid, best);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,88 +269,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
|||||||
bestCluster.set(tx.txid, tx);
|
bestCluster.set(tx.txid, tx);
|
||||||
|
|
||||||
return bestCluster;
|
return bestCluster;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
|
||||||
* and update the survivors' scores and ancestors
|
|
||||||
*
|
|
||||||
* @param cluster
|
|
||||||
* @param ancestors
|
|
||||||
*/
|
|
||||||
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
|
||||||
// remove
|
|
||||||
cluster.forEach(tx => {
|
|
||||||
all.delete(tx.txid);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update survivors
|
|
||||||
all.forEach(tx => {
|
|
||||||
cluster.forEach(remove => {
|
|
||||||
if (tx.ancestorMap?.has(remove.txid)) {
|
|
||||||
// remove as dependency
|
|
||||||
tx.ancestorMap.delete(remove.txid);
|
|
||||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
|
||||||
// update ancestor sizes and fees
|
|
||||||
tx.ancestorsize -= remove.adjustedVsize;
|
|
||||||
tx.fees.ancestor -= remove.fees.base;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// recalculate fee rates
|
|
||||||
setAncestorScores(tx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
|
||||||
* for each transaction.
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
* @param all
|
|
||||||
*/
|
|
||||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
|
||||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
|
||||||
if (depth > MAX_GRAPH_SIZE) {
|
|
||||||
return tx.ancestorMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the ancestor map for this tx
|
|
||||||
tx.ancestorMap = new Map<string, GraphTx>();
|
|
||||||
tx.depends.forEach(parentId => {
|
|
||||||
const parent = all.get(parentId);
|
|
||||||
if (parent) {
|
|
||||||
// add the parent
|
|
||||||
tx.ancestorMap?.set(parentId, parent);
|
|
||||||
// check for a cached copy of this parent's ancestors
|
|
||||||
let ancestors = visited.get(parent.txid);
|
|
||||||
if (!ancestors) {
|
|
||||||
// recursively fetch the parent's ancestors
|
|
||||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
|
||||||
}
|
|
||||||
// and add to this tx's map
|
|
||||||
ancestors.forEach((ancestor, ancestorId) => {
|
|
||||||
tx.ancestorMap?.set(ancestorId, ancestor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
visited.set(tx.txid, tx.ancestorMap);
|
|
||||||
|
|
||||||
return tx.ancestorMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
|
||||||
*
|
|
||||||
* @param tx
|
|
||||||
*/
|
|
||||||
function setAncestorScores(tx: GraphTx): GraphTx {
|
|
||||||
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
|
|
||||||
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
|
|
||||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by descending score
|
|
||||||
function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
|
||||||
return b.score - a.score;
|
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 76;
|
private static currentVersion = 82;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@@ -652,6 +652,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||||
|
|
||||||
|
if (isBitcoin === true) {
|
||||||
|
await this.$executeQuery('TRUNCATE hashrates');
|
||||||
|
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||||
|
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateToSchemaVersion(75);
|
await this.updateToSchemaVersion(75);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,6 +666,45 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
||||||
await this.updateToSchemaVersion(76);
|
await this.updateToSchemaVersion(76);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 77 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||||
|
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
|
||||||
|
await this.updateToSchemaVersion(77);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 78) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL');
|
||||||
|
await this.updateToSchemaVersion(78);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 79 && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||||
|
// Clear bad data
|
||||||
|
await this.$executeQuery(`TRUNCATE accelerations`);
|
||||||
|
this.uniqueLog(logger.notice, `'accelerations' table has been truncated`);
|
||||||
|
await this.$executeQuery(`
|
||||||
|
UPDATE state
|
||||||
|
SET number = 0
|
||||||
|
WHERE name = 'last_acceleration_block'
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(79);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 80) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
|
||||||
|
await this.updateToSchemaVersion(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 81 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(81);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||||
|
await this.$fixBadV1AuditBlocks();
|
||||||
|
await this.updateToSchemaVersion(82);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1273,6 +1319,28 @@ class DatabaseMigration {
|
|||||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||||
|
const badBlocks = [
|
||||||
|
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||||
|
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||||
|
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||||
|
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||||
|
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const hash of badBlocks) {
|
||||||
|
try {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
UPDATE blocks_audits
|
||||||
|
SET prioritized_txs = '[]'
|
||||||
|
WHERE hash = '${hash}'
|
||||||
|
`, true);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ class DiskCache {
|
|||||||
trees: rbfData.rbf.trees,
|
trees: rbfData.rbf.trees,
|
||||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class ChannelsRoutes {
|
class ChannelsRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -22,7 +23,7 @@ class ChannelsRoutes {
|
|||||||
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channel);
|
res.json(channel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +54,12 @@ class ChannelsRoutes {
|
|||||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||||
|
|
||||||
if (index < -1) {
|
if (index < -1) {
|
||||||
res.status(400).send('Invalid index');
|
handleError(req, res, 400, 'Invalid index');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||||
res.status(400).send('Invalid status');
|
handleError(req, res, 400, 'Invalid status');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||||
@@ -67,14 +70,14 @@ class ChannelsRoutes {
|
|||||||
res.header('X-Total-Count', channelsCount.toString());
|
res.header('X-Total-Count', channelsCount.toString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(400).send('Not an array');
|
handleError(req, res, 400, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@@ -105,7 +108,7 @@ class ChannelsRoutes {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +120,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +133,7 @@ class ChannelsRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
import statisticsApi from './statistics.api';
|
import statisticsApi from './statistics.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class GeneralLightningRoutes {
|
class GeneralLightningRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
|||||||
channels: channels,
|
channels: channels,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ class GeneralLightningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class GeneralLightningRoutes {
|
|||||||
const statistics = await statisticsApi.$getLatestStatistics();
|
const statistics = await statisticsApi.$getLatestStatistics();
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -666,7 +666,9 @@ class NodesApi {
|
|||||||
node.last_update = null;
|
node.last_update = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
|
const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))];
|
||||||
|
const formattedSockets = (uniqueAddr.join(',')) ?? '';
|
||||||
|
|
||||||
const query = `INSERT INTO nodes(
|
const query = `INSERT INTO nodes(
|
||||||
public_key,
|
public_key,
|
||||||
first_seen,
|
first_seen,
|
||||||
@@ -695,13 +697,13 @@ class NodesApi {
|
|||||||
node.alias,
|
node.alias,
|
||||||
this.aliasToSearchText(node.alias),
|
this.aliasToSearchText(node.alias),
|
||||||
node.color,
|
node.color,
|
||||||
sockets,
|
formattedSockets,
|
||||||
JSON.stringify(node.features),
|
JSON.stringify(node.features),
|
||||||
node.last_update,
|
node.last_update,
|
||||||
node.alias,
|
node.alias,
|
||||||
this.aliasToSearchText(node.alias),
|
this.aliasToSearchText(node.alias),
|
||||||
node.color,
|
node.color,
|
||||||
sockets,
|
formattedSockets,
|
||||||
JSON.stringify(node.features),
|
JSON.stringify(node.features),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -713,7 +715,9 @@ class NodesApi {
|
|||||||
* Update node sockets
|
* Update node sockets
|
||||||
*/
|
*/
|
||||||
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
|
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
|
||||||
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
|
const uniqueAddr = [...new Set(sockets.map(a => a.addr))];
|
||||||
|
|
||||||
|
const formattedSockets = (uniqueAddr.join(',')) ?? '';
|
||||||
try {
|
try {
|
||||||
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
|
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import { INodesRanking } from '../../mempool.interfaces';
|
import { INodesRanking } from '../../mempool.interfaces';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -31,7 +32,7 @@ class NodesRoutes {
|
|||||||
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
||||||
res.json(nodes);
|
res.json(nodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,14 @@ class NodesRoutes {
|
|||||||
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
|
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
|
||||||
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
|
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
|
||||||
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
|
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
|
||||||
|
'02b36a324fa2dd3af2a63ac65f241907882829bed5002b4e14171d25c219e0d470',
|
||||||
|
'0231b6e8f21f9f6c057f6bf8a812f79e396ee16a66ece91939a1576ce9fb9e87a5',
|
||||||
|
'034b6aac206bffcbd651b7ead1ab8a0991c945dfafe19ff27dcdeadc6843ebd15c',
|
||||||
|
'039c065f7e344acd969ebdd4a94550915b6f24e8782ae2be540bb96c8a4fcfb86b',
|
||||||
|
'03d9f9f4803fc75920f14dd13d83fbecc53229a65d4ee4cd2d86fdf211f7337576',
|
||||||
|
'0357fe48c4dece744f70865eda66e396aab5d05e09e1145cd3b7da83f11446d4cf',
|
||||||
|
'02bca4d642eda631f2c8659758e2a2868e518b93503f2bfcd767749c6530a10679',
|
||||||
|
'03f32c99c0bb9f62dae53671d1d300565773455248f34134cc02779b881561174e',
|
||||||
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
||||||
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
||||||
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
||||||
@@ -60,6 +69,14 @@ class NodesRoutes {
|
|||||||
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
|
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
|
||||||
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
|
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
|
||||||
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
|
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
|
||||||
|
'038eb09bed4532ff36d12acc1279f55cbe8d95212d19f809e057bb50de00051fba',
|
||||||
|
'027b7c0278366a0268e8bd0072b14539f6cb455a7bd588ae22d888bed541f65311',
|
||||||
|
'02f4dd78f6eda8838029b2cdbaaea6e875e2fa373cd348ee41a7c1bb177d3fca66',
|
||||||
|
'036b3fb692da214a3edaac5b67903b958f5ccd8712e09aa61b67ea7acfd94b40c2',
|
||||||
|
'023bc8915d308e0b65f8de6867f95960141372436fce3edad5cec3f364d6ac948f',
|
||||||
|
'0341690503ef21d0e203dddd9e62646380d0dfc32c499e055e7f698b9064d1c736',
|
||||||
|
'0355d573805c018a37a5b2288378d70e9b5b438f7394abd6f467cb9b47c90eeb93',
|
||||||
|
'0361aa68deb561a8b47b41165848edcccb98a1b56a5ea922d9d5b30a09bb7282ea',
|
||||||
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
|
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
|
||||||
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
|
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
|
||||||
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
|
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
|
||||||
@@ -76,6 +93,14 @@ class NodesRoutes {
|
|||||||
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
|
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
|
||||||
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
|
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
|
||||||
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
|
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
|
||||||
|
'030bbbd8495561a894e301fe6ba5b22f8941fc661cc0e673e0206158231d8ac130',
|
||||||
|
'03ee1f08e516ed083475f39c6cae4fa1eec686d004d2f105218269e27d7f2da5a4',
|
||||||
|
'028c378b998f476ed22d6815c170dd2a3388a43fdf791a7cff70b9997349b8447a',
|
||||||
|
'036f19f044d19cb1b04f14d91b6e7e5443ce337217a8c14d43861f3e86dd07bd7f',
|
||||||
|
'03058d61869e8b88436493648b2e3e530627edf5a0b253c285cd565c1477a5c237',
|
||||||
|
'0279dfedc87b47a941f1797f2c422c03aa3108914ea6b519d76537d60860535a9a',
|
||||||
|
'0353486b8016761e58ec8aee7305ee58d5dc66b55ef5bd8cbaf49508f66d52d62e',
|
||||||
|
'03df5db8eccfabcae47ff15553cfdecb2d3f56979f43a0c3578f28d056b5e35104',
|
||||||
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
||||||
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
||||||
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
||||||
@@ -88,6 +113,14 @@ class NodesRoutes {
|
|||||||
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
|
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
|
||||||
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
|
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
|
||||||
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
|
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
|
||||||
|
'0326cf9a4ca67a5b9cdffae57293dbd6f7c5113b93010dc6f6fe4af3afde1a1739',
|
||||||
|
'034867e16f62cebb8c2c2c22b91117c173bbece9c8a1e5bd001374a3699551cd8f',
|
||||||
|
'038dfb1f1b637a8c27e342ffc6f9feca20e0b47be3244e09ae78df4998e2ae83b9',
|
||||||
|
'03cb1cea3394d973355c11bc61c2f689f9d3e1c3db60d205f27770f5ad83200f77',
|
||||||
|
'03535447b592cbdb153189b3e06a455452b1011380cb3e6511a31090c15d8efc9f',
|
||||||
|
'028e90e9984d262ebfa3c23fb3f335a2ae061a0bdedee03f45f72b438d9e7d2ce3',
|
||||||
|
'03ee0176289dc4a6111fa5ef22eed5273758c420fbe58cc1d2d76def75dd7e640c',
|
||||||
|
'0370b2cd9f0eaf436d5c25c93fb39210d8cc06b31f688fc2f54418aabe394aed79',
|
||||||
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
|
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
|
||||||
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
|
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
|
||||||
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
|
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
|
||||||
@@ -104,6 +137,14 @@ class NodesRoutes {
|
|||||||
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
|
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
|
||||||
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
|
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
|
||||||
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
|
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
|
||||||
|
'021b28ecdd782fd909705d6be354db268977b1a2ac5a5275186fc19e08bb8fca93',
|
||||||
|
'031bec1fbd8eb7fe94d2bda108c9c3cc8c22ecfc1c3a5c11d36f5881b01b4a81a6',
|
||||||
|
'03879c4f827a3188574d5757e002f574265a966d70aea942169785b31369b067d5',
|
||||||
|
'0228d4b5a4fd73a03967b76f8b8cb37b9d0b6e7039126a9397bb732c15bed78e9b',
|
||||||
|
'03f58dbb629f4427f5a1dbc02e6a7ec79345fdf13a0e4163d4f3b7aea2539cf095',
|
||||||
|
'021cdcb8123aa670cdfc9f43909dbb297363c093883409e9e7fc82e7267f7c72bd',
|
||||||
|
'02f2aa2c2b7b432a70dc4d0b04afa19d48715ed3b90594d49c1c8744f2e9ebb030',
|
||||||
|
'03709a02fb3ab4857689a8ea0bd489a6ab6f56f8a397be578bc6d5ad22efbe3756',
|
||||||
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
||||||
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
||||||
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
||||||
@@ -116,6 +157,14 @@ class NodesRoutes {
|
|||||||
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
|
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
|
||||||
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
|
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
|
||||||
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
|
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
|
||||||
|
'03b4dda7878d3b7b71ecd6d4738322c7f9a9c1fb583374d2724f4ccc4947f37570',
|
||||||
|
'0279a35f05b5acf159429549e56fd426685c4fec191431c58738968bbc77a39f25',
|
||||||
|
'03cb102d796ddcf08610cd03fae8b7a1df69ff48e9e8a152af315f9edf71762eb8',
|
||||||
|
'036b89526f4d5ac4c317f4fd23cb9f8e4ad844498bc7950a41114d060101d995d4',
|
||||||
|
'0313eade145959d7036db009fd5b0bf1947a739c7c3c790b491ec9161b94e6ad1e',
|
||||||
|
'02b670ca4c4bb2c5ea89c3b691da98a194cfc48fcd5c072df02a20290bddd60610',
|
||||||
|
'02a9196d5e08598211397a83cf013a5962b84bd61198abfdd204dff987e54f7a0d',
|
||||||
|
'036d015cd2f486fb38348182980b7e596e6c9733873102ea126fed7b4152be03b8',
|
||||||
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
|
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
|
||||||
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
|
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
|
||||||
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',
|
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',
|
||||||
@@ -133,13 +182,13 @@ class NodesRoutes {
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(nodes);
|
res.json(nodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +196,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getNode(req.params.public_key);
|
const node = await nodesApi.$getNode(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@@ -155,7 +204,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(node);
|
res.json(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +216,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +224,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@@ -183,7 +232,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(node);
|
res.json(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +248,7 @@ class NodesRoutes {
|
|||||||
topByChannels: topChannelsNodes,
|
topByChannels: topChannelsNodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +260,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +272,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +284,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(topCapacityNodes);
|
res.json(topCapacityNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +296,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(nodesPerAs);
|
res.json(nodesPerAs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +308,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(worldNodes);
|
res.json(worldNodes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +323,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (country.length === 0) {
|
if (country.length === 0) {
|
||||||
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +336,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +350,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isp.length === 0) {
|
if (isp.length === 0) {
|
||||||
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +363,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +375,7 @@ class NodesRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||||
res.json(nodesPerAs);
|
res.json(nodesPerAs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import elementsParser from './elements-parser';
|
import elementsParser from './elements-parser';
|
||||||
import icons from './icons';
|
import icons from './icons';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class LiquidRoutes {
|
class LiquidRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@@ -42,7 +43,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('content-length', result.length);
|
res.setHeader('content-length', result.length);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icon not found');
|
handleError(req, res, 404, 'Asset icon not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ class LiquidRoutes {
|
|||||||
if (result) {
|
if (result) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icons not found');
|
handleError(req, res, 404, 'Asset icons not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
res.json(pegs);
|
res.json(pegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
res.json(reserves);
|
res.json(reserves);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(currentSupply);
|
res.json(currentSupply);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(currentReserves);
|
res.json(currentReserves);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(auditStatus);
|
res.json(auditStatus);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationAddresses);
|
res.json(federationAddresses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationAddresses);
|
res.json(federationAddresses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationUtxos);
|
res.json(federationUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +179,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(expiredUtxos);
|
res.json(expiredUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(federationUtxos);
|
res.json(federationUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(emergencySpentUtxos);
|
res.json(emergencySpentUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +215,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(emergencySpentUtxos);
|
res.json(emergencySpentUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(recentPegs);
|
res.json(recentPegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(pegsVolume);
|
res.json(pegsVolume);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(pegsCount);
|
res.json(pegsCount);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
|
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces';
|
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces';
|
||||||
import { Common, OnlineFeeStatsCalculator } from './common';
|
import { Common, OnlineFeeStatsCalculator } from './common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
|
import { Acceleration } from './services/acceleration';
|
||||||
|
import PoolsRepository from '../repositories/PoolsRepository';
|
||||||
|
|
||||||
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
||||||
|
|
||||||
@@ -14,12 +16,14 @@ class MempoolBlocks {
|
|||||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||||
private txSelectionWorker: Worker | null = null;
|
private txSelectionWorker: Worker | null = null;
|
||||||
private rustInitialized: boolean = false;
|
private rustInitialized: boolean = false;
|
||||||
private rustGbtGenerator: GbtGenerator = new GbtGenerator();
|
private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
private nextUid: number = 1;
|
private nextUid: number = 1;
|
||||||
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
||||||
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
|
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
|
||||||
|
|
||||||
|
private pools: { [id: number]: PoolTag } = {};
|
||||||
|
|
||||||
public getMempoolBlocks(): MempoolBlock[] {
|
public getMempoolBlocks(): MempoolBlock[] {
|
||||||
return this.mempoolBlocks.map((block) => {
|
return this.mempoolBlocks.map((block) => {
|
||||||
return {
|
return {
|
||||||
@@ -41,6 +45,18 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlockDeltas;
|
return this.mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updatePools$(): Promise<void> {
|
||||||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||||
|
this.pools = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allPools = await PoolsRepository.$getPools();
|
||||||
|
this.pools = {};
|
||||||
|
for (const pool of allPools) {
|
||||||
|
this.pools[pool.uniqueId] = pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
|
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
|
||||||
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||||
@@ -214,7 +230,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
private resetRustGbt(): void {
|
private resetRustGbt(): void {
|
||||||
this.rustInitialized = false;
|
this.rustInitialized = false;
|
||||||
this.rustGbtGenerator = new GbtGenerator();
|
this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||||
@@ -246,7 +262,7 @@ class MempoolBlocks {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// run the block construction algorithm in a separate thread, and wait for a result
|
// run the block construction algorithm in a separate thread, and wait for a result
|
||||||
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
|
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||||
try {
|
try {
|
||||||
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
|
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
|
||||||
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
||||||
@@ -333,24 +349,27 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations: { [txid: string]: Acceleration }, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||||
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
|
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
|
||||||
if (txid in mempool) {
|
if (txid in mempool) {
|
||||||
mempool[txid].cpfpDirty = false;
|
mempool[txid].cpfpDirty = false;
|
||||||
|
mempool[txid].ancestors = [];
|
||||||
|
mempool[txid].descendants = [];
|
||||||
|
mempool[txid].bestDescendant = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [txid, rate] of rates) {
|
for (const [txid, rate] of rates) {
|
||||||
if (txid in mempool) {
|
if (txid in mempool) {
|
||||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||||
mempool[txid].effectiveFeePerVsize = rate;
|
mempool[txid].effectiveFeePerVsize = rate;
|
||||||
mempool[txid].cpfpChecked = false;
|
mempool[txid].cpfpChecked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastBlockIndex = blocks.length - 1;
|
const lastBlockIndex = blocks.length - 1;
|
||||||
let hasBlockStack = blocks.length >= 8;
|
let hasBlockStack = blocks.length >= 8;
|
||||||
let stackWeight;
|
let stackWeight;
|
||||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||||
if (hasBlockStack) {
|
if (hasBlockStack) {
|
||||||
if (blockWeights && blockWeights[7] !== null) {
|
if (blockWeights && blockWeights[7] !== null) {
|
||||||
stackWeight = blockWeights[7];
|
stackWeight = blockWeights[7];
|
||||||
@@ -361,28 +380,36 @@ class MempoolBlocks {
|
|||||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
let ancestor: MempoolTransactionExtended
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
for (const memberTxid of cluster) {
|
for (const memberTxid of cluster) {
|
||||||
const mempoolTx = mempool[memberTxid];
|
const mempoolTx = mempool[memberTxid];
|
||||||
if (mempoolTx) {
|
if (mempoolTx) {
|
||||||
const ancestors: Ancestor[] = [];
|
// ugly micro-optimization to avoid allocating new arrays
|
||||||
const descendants: Ancestor[] = [];
|
ancestors.length = 0;
|
||||||
|
descendants.length = 0;
|
||||||
let matched = false;
|
let matched = false;
|
||||||
cluster.forEach(txid => {
|
cluster.forEach(txid => {
|
||||||
|
ancestor = mempool[txid];
|
||||||
if (txid === memberTxid) {
|
if (txid === memberTxid) {
|
||||||
matched = true;
|
matched = true;
|
||||||
} else {
|
} else {
|
||||||
if (!mempool[txid]) {
|
if (!ancestor) {
|
||||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const relative = {
|
const relative = {
|
||||||
txid: txid,
|
txid: txid,
|
||||||
fee: mempool[txid].fee,
|
fee: ancestor.fee,
|
||||||
weight: (mempool[txid].adjustedVsize * 4),
|
weight: (ancestor.adjustedVsize * 4),
|
||||||
};
|
};
|
||||||
if (matched) {
|
if (matched) {
|
||||||
descendants.push(relative);
|
descendants.push(relative);
|
||||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
|
||||||
|
mempoolTx.lastBoosted = ancestor.firstSeen;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ancestors.push(relative);
|
ancestors.push(relative);
|
||||||
}
|
}
|
||||||
@@ -391,17 +418,33 @@ class MempoolBlocks {
|
|||||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
}
|
}
|
||||||
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
|
// ugly micro-optimization to avoid allocating new arrays or objects
|
||||||
|
if (mempoolTx.ancestors) {
|
||||||
|
mempoolTx.ancestors.length = 0;
|
||||||
|
} else {
|
||||||
|
mempoolTx.ancestors = [];
|
||||||
|
}
|
||||||
|
if (mempoolTx.descendants) {
|
||||||
|
mempoolTx.descendants.length = 0;
|
||||||
|
} else {
|
||||||
|
mempoolTx.descendants = [];
|
||||||
|
}
|
||||||
|
mempoolTx.ancestors.push(...ancestors);
|
||||||
|
mempoolTx.descendants.push(...descendants);
|
||||||
|
mempoolTx.cpfpChecked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAccelerated : { [txid: string]: boolean } = {};
|
const isAcceleratedBy : { [txid: string]: number[] | false } = {};
|
||||||
|
|
||||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||||
// update this thread's mempool with the results
|
// update this thread's mempool with the results
|
||||||
let mempoolTx: MempoolTransactionExtended;
|
let mempoolTx: MempoolTransactionExtended;
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
|
let acceleration: Acceleration;
|
||||||
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
|
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||||
|
const block = blocks[blockIndex];
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
let totalVsize = 0;
|
let totalVsize = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
@@ -417,7 +460,8 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const txid of block) {
|
for (let i = 0; i < block.length; i++) {
|
||||||
|
const txid = block[i];
|
||||||
if (txid) {
|
if (txid) {
|
||||||
mempoolTx = mempool[txid];
|
mempoolTx = mempool[txid];
|
||||||
// save position in projected blocks
|
// save position in projected blocks
|
||||||
@@ -426,24 +470,37 @@ class MempoolBlocks {
|
|||||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceleration = accelerations[txid];
|
if (txid in accelerations) {
|
||||||
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
acceleration = accelerations[txid];
|
||||||
if (!mempoolTx.acceleration) {
|
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||||
mempoolTx.cpfpDirty = true;
|
if (!mempoolTx.acceleration) {
|
||||||
}
|
mempoolTx.cpfpDirty = true;
|
||||||
mempoolTx.acceleration = true;
|
}
|
||||||
for (const ancestor of mempoolTx.ancestors || []) {
|
mempoolTx.acceleration = true;
|
||||||
if (!mempool[ancestor.txid].acceleration) {
|
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||||
mempool[ancestor.txid].cpfpDirty = true;
|
mempoolTx.acceleratedAt = acceleration?.added;
|
||||||
|
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||||
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
|
if (!mempool[ancestor.txid].acceleration) {
|
||||||
|
mempool[ancestor.txid].cpfpDirty = true;
|
||||||
|
}
|
||||||
|
mempool[ancestor.txid].acceleration = true;
|
||||||
|
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||||
|
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||||
|
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||||
|
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mempoolTx.acceleration) {
|
||||||
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
mempool[ancestor.txid].acceleration = true;
|
|
||||||
isAccelerated[ancestor.txid] = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mempoolTx.acceleration) {
|
if (mempoolTx.acceleration) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
delete mempoolTx.acceleration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// online calculation of stack-of-blocks fee stats
|
// online calculation of stack-of-blocks fee stats
|
||||||
@@ -461,7 +518,7 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.dataToMempoolBlocks(
|
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||||
block,
|
block,
|
||||||
transactions,
|
transactions,
|
||||||
totalSize,
|
totalSize,
|
||||||
@@ -469,13 +526,13 @@ class MempoolBlocks {
|
|||||||
totalFees,
|
totalFees,
|
||||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
if (saveResults) {
|
if (saveResults) {
|
||||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||||
this.mempoolBlocks = mempoolBlocks;
|
this.mempoolBlocks = mempoolBlocks;
|
||||||
this.mempoolBlockDeltas = deltas;
|
this.mempoolBlockDeltas = deltas;
|
||||||
|
this.updateAccelerationPositions(mempool, accelerations, mempoolBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mempoolBlocks;
|
return mempoolBlocks;
|
||||||
@@ -622,6 +679,124 @@ class MempoolBlocks {
|
|||||||
tx.acc ? 1 : 0,
|
tx.acc ? 1 : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// estimates and saves positions of accelerations in mining partner mempools
|
||||||
|
private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void {
|
||||||
|
const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||||
|
// keep track of simulated mempool blocks for each active pool
|
||||||
|
const pools: {
|
||||||
|
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
||||||
|
} = {};
|
||||||
|
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
||||||
|
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
|
||||||
|
let vsize = mempoolCache[acc.txid].vsize;
|
||||||
|
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
||||||
|
vsize += (ancestor.weight / 4);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
acceleration: acc,
|
||||||
|
rate: mempoolCache[acc.txid].effectiveFeePerVsize,
|
||||||
|
vsize
|
||||||
|
};
|
||||||
|
}).sort((a, b) => a.rate - b.rate);
|
||||||
|
// initialize the pool tracker
|
||||||
|
for (const { acceleration } of accQueue) {
|
||||||
|
accelerationPositions[acceleration.txid] = [];
|
||||||
|
for (const pool of acceleration.pools) {
|
||||||
|
if (!pools[pool]) {
|
||||||
|
pools[pool] = {
|
||||||
|
name: this.pools[pool]?.name || 'unknown',
|
||||||
|
block: 0,
|
||||||
|
vsize: 0,
|
||||||
|
accelerations: [],
|
||||||
|
complete: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pools[pool].accelerations.push(acceleration.txid);
|
||||||
|
}
|
||||||
|
for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) {
|
||||||
|
accelerationPositions[ancestor.txid] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pool of Object.keys(pools)) {
|
||||||
|
// if any pools accepted *every* acceleration, we can just use the GBT result positions directly
|
||||||
|
if (pools[pool].accelerations.length === Object.keys(accelerations).length) {
|
||||||
|
pools[pool].complete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = 0;
|
||||||
|
let index = 0;
|
||||||
|
let next = accQueue.pop();
|
||||||
|
// build simulated blocks for each pool by taking the best option from
|
||||||
|
// either the mempool or the list of accelerations.
|
||||||
|
while (next && block < mempoolBlocks.length) {
|
||||||
|
while (next && index < mempoolBlocks[block].transactions.length) {
|
||||||
|
const nextTx = mempoolBlocks[block].transactions[index];
|
||||||
|
if (next.rate >= (nextTx.rate || (nextTx.fee / nextTx.vsize))) {
|
||||||
|
for (const pool of next.acceleration.pools) {
|
||||||
|
if (pools[pool].vsize + next.vsize <= 999_000) {
|
||||||
|
pools[pool].vsize += next.vsize;
|
||||||
|
} else {
|
||||||
|
pools[pool].block++;
|
||||||
|
pools[pool].vsize = next.vsize;
|
||||||
|
}
|
||||||
|
// insert the acceleration into matching pool's blocks
|
||||||
|
if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) {
|
||||||
|
accelerationPositions[next.acceleration.txid].push({
|
||||||
|
...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number },
|
||||||
|
poolId: pool,
|
||||||
|
pool: pools[pool].name
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
accelerationPositions[next.acceleration.txid].push({
|
||||||
|
poolId: pool,
|
||||||
|
pool: pools[pool].name,
|
||||||
|
block: pools[pool].block,
|
||||||
|
vsize: pools[pool].vsize - (next.vsize / 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// and any accelerated ancestors
|
||||||
|
for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) {
|
||||||
|
if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) {
|
||||||
|
accelerationPositions[ancestor.txid].push({
|
||||||
|
...mempoolCache[ancestor.txid].position as { block: number, vsize: number },
|
||||||
|
poolId: pool,
|
||||||
|
pool: pools[pool].name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
accelerationPositions[ancestor.txid].push({
|
||||||
|
poolId: pool,
|
||||||
|
pool: pools[pool].name,
|
||||||
|
block: pools[pool].block,
|
||||||
|
vsize: pools[pool].vsize - (next.vsize / 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next = accQueue.pop();
|
||||||
|
} else {
|
||||||
|
// skip accelerated transactions and their CPFP ancestors
|
||||||
|
if (accelerationPositions[nextTx.txid] == null) {
|
||||||
|
// insert into all pools' blocks
|
||||||
|
for (const pool of Object.keys(pools)) {
|
||||||
|
if (pools[pool].vsize + nextTx.vsize <= 999_000) {
|
||||||
|
pools[pool].vsize += nextTx.vsize;
|
||||||
|
} else {
|
||||||
|
pools[pool].block++;
|
||||||
|
pools[pool].vsize = nextTx.vsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block++;
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
mempool.setAccelerationPositions(accelerationPositions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MempoolBlocks();
|
export default new MempoolBlocks();
|
||||||
|
|||||||
@@ -19,14 +19,16 @@ class Mempool {
|
|||||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||||
|
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||||
|
|
||||||
private accelerations: { [txId: string]: Acceleration } = {};
|
private accelerations: { [txId: string]: Acceleration } = {};
|
||||||
|
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||||
|
|
||||||
private txPerSecondArray: number[] = [];
|
private txPerSecondArray: number[] = [];
|
||||||
private txPerSecond: number = 0;
|
private txPerSecond: number = 0;
|
||||||
@@ -73,12 +75,12 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates) => Promise<void>): void {
|
candidates?: GbtCandidates) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
@@ -361,12 +363,15 @@ class Mempool {
|
|||||||
|
|
||||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
this.recentlyDeleted.unshift(deletedTransactions);
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||||
|
|
||||||
|
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||||
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
|
||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,15 +400,15 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const changed: string[] = [];
|
const changed: string[] = [];
|
||||||
|
|
||||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||||
for (const acceleration of newAccelerations) {
|
for (const acceleration of newAccelerations) {
|
||||||
|
// skip transactions we don't know about
|
||||||
|
if (!this.mempoolCache[acceleration.txid]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
newAccelerationMap[acceleration.txid] = acceleration;
|
newAccelerationMap[acceleration.txid] = acceleration;
|
||||||
if (this.accelerations[acceleration.txid] == null) {
|
if (this.accelerations[acceleration.txid] == null) {
|
||||||
// new acceleration
|
// new acceleration
|
||||||
@@ -510,6 +515,14 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void {
|
||||||
|
this.accelerationPositions = positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined {
|
||||||
|
return this.accelerationPositions[txid];
|
||||||
|
}
|
||||||
|
|
||||||
private startTimer() {
|
private startTimer() {
|
||||||
const state: any = {
|
const state: any = {
|
||||||
start: Date.now(),
|
start: Date.now(),
|
||||||
@@ -532,16 +545,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
|
||||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
|
||||||
// Store replaced transactions
|
|
||||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
|
|||||||
515
backend/src/api/mini-miner.ts
Normal file
515
backend/src/api/mini-miner.ts
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
import { Acceleration } from './acceleration/acceleration';
|
||||||
|
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
|
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||||
|
const BLOCK_SIGOPS = 80_000;
|
||||||
|
const MAX_RELATIVE_GRAPH_SIZE = 100;
|
||||||
|
|
||||||
|
export interface GraphTx {
|
||||||
|
txid: string;
|
||||||
|
vsize: number;
|
||||||
|
weight: number;
|
||||||
|
depends: string[];
|
||||||
|
spentby: string[];
|
||||||
|
|
||||||
|
ancestorcount: number;
|
||||||
|
ancestorsize: number;
|
||||||
|
fees: { // in sats
|
||||||
|
base: number;
|
||||||
|
ancestor: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
ancestors: Map<string, GraphTx>,
|
||||||
|
ancestorRate: number;
|
||||||
|
individualRate: number;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateTransaction {
|
||||||
|
txid: string;
|
||||||
|
order: number;
|
||||||
|
weight: number;
|
||||||
|
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||||
|
sigops: number;
|
||||||
|
fee: number;
|
||||||
|
feeDelta: number;
|
||||||
|
ancestors: string[];
|
||||||
|
cluster: string[];
|
||||||
|
effectiveFeePerVsize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MinerTransaction extends TemplateTransaction {
|
||||||
|
inputs: string[];
|
||||||
|
feePerVsize: number;
|
||||||
|
relativesSet: boolean;
|
||||||
|
ancestorMap: Map<string, MinerTransaction>;
|
||||||
|
children: Set<MinerTransaction>;
|
||||||
|
ancestorFee: number;
|
||||||
|
ancestorVsize: number;
|
||||||
|
ancestorSigops: number;
|
||||||
|
score: number;
|
||||||
|
used: boolean;
|
||||||
|
modified: boolean;
|
||||||
|
dependencyRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a raw transaction, and builds a graph of same-block relatives,
|
||||||
|
* and returns as a GraphTx
|
||||||
|
*
|
||||||
|
* @param tx
|
||||||
|
*/
|
||||||
|
export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
||||||
|
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||||
|
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||||
|
for (const tx of transactions) {
|
||||||
|
blockTxs.set(tx.txid, tx);
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatives: Map<string, GraphTx> = new Map();
|
||||||
|
const stack: string[] = [tx.txid];
|
||||||
|
|
||||||
|
// build set of same-block ancestors
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const nextTxid = stack.pop();
|
||||||
|
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||||
|
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mempoolTx = convertToGraphTx(nextTx, spendMap);
|
||||||
|
|
||||||
|
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||||
|
if (txid) {
|
||||||
|
stack.push(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relatives.set(mempoolTx.txid, mempoolTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return relatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a raw transaction and converts it to GraphTx format
|
||||||
|
* fee and ancestor data is initialized with dummy/null values
|
||||||
|
*
|
||||||
|
* @param tx
|
||||||
|
*/
|
||||||
|
export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx {
|
||||||
|
return {
|
||||||
|
txid: tx.txid,
|
||||||
|
vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||||
|
weight: tx.weight,
|
||||||
|
fees: {
|
||||||
|
base: tx.fee || 0,
|
||||||
|
ancestor: tx.fee || 0,
|
||||||
|
},
|
||||||
|
depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]),
|
||||||
|
spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [],
|
||||||
|
|
||||||
|
ancestorcount: 1,
|
||||||
|
ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||||
|
ancestors: new Map<string, GraphTx>(),
|
||||||
|
ancestorRate: 0,
|
||||||
|
individualRate: 0,
|
||||||
|
score: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
||||||
|
*/
|
||||||
|
export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> {
|
||||||
|
const relatives: Map<string, GraphTx> = new Map();
|
||||||
|
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||||
|
while (stack.length > 0) {
|
||||||
|
if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) {
|
||||||
|
return relatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTx = stack.pop();
|
||||||
|
if (!nextTx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
relatives.set(nextTx.txid, nextTx);
|
||||||
|
|
||||||
|
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||||
|
if (relatives.has(relativeTxid)) {
|
||||||
|
// already processed this tx
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ancestorTx = ancestors.get(relativeTxid);
|
||||||
|
if (!ancestorTx && relativeTxid in mempool) {
|
||||||
|
const mempoolTx = mempool[relativeTxid];
|
||||||
|
ancestorTx = convertToGraphTx(mempoolTx, spendMap);
|
||||||
|
}
|
||||||
|
if (ancestorTx) {
|
||||||
|
stack.push(ancestorTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||||
|
* for each transaction.
|
||||||
|
*
|
||||||
|
* @param tx
|
||||||
|
* @param all
|
||||||
|
*/
|
||||||
|
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||||
|
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||||
|
if (depth > MAX_RELATIVE_GRAPH_SIZE) {
|
||||||
|
logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed');
|
||||||
|
return tx.ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the ancestor map for this tx
|
||||||
|
tx.ancestors = new Map<string, GraphTx>();
|
||||||
|
tx.depends.forEach(parentId => {
|
||||||
|
const parent = all.get(parentId);
|
||||||
|
if (parent) {
|
||||||
|
// add the parent
|
||||||
|
tx.ancestors?.set(parentId, parent);
|
||||||
|
// check for a cached copy of this parent's ancestors
|
||||||
|
let ancestors = visited.get(parent.txid);
|
||||||
|
if (!ancestors) {
|
||||||
|
// recursively fetch the parent's ancestors
|
||||||
|
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||||
|
}
|
||||||
|
// and add to this tx's map
|
||||||
|
ancestors.forEach((ancestor, ancestorId) => {
|
||||||
|
tx.ancestors?.set(ancestorId, ancestor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
visited.set(tx.txid, tx.ancestors);
|
||||||
|
|
||||||
|
return tx.ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||||
|
* by running setAncestors on each leaf, and caching intermediate results.
|
||||||
|
* then initializes ancestor data for each transaction
|
||||||
|
*
|
||||||
|
* @param all
|
||||||
|
*/
|
||||||
|
export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||||
|
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||||
|
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||||
|
for (const leaf of leaves) {
|
||||||
|
setAncestors(leaf, mempoolTxs, visited);
|
||||||
|
}
|
||||||
|
mempoolTxs.forEach(entry => {
|
||||||
|
entry.ancestors?.forEach(ancestor => {
|
||||||
|
entry.ancestorcount++;
|
||||||
|
entry.ancestorsize += ancestor.vsize;
|
||||||
|
entry.fees.ancestor += ancestor.fees.base;
|
||||||
|
});
|
||||||
|
setAncestorScores(entry);
|
||||||
|
});
|
||||||
|
return mempoolTxs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||||
|
* and update the survivors' scores and ancestors
|
||||||
|
*
|
||||||
|
* @param cluster
|
||||||
|
* @param ancestors
|
||||||
|
*/
|
||||||
|
export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||||
|
// remove
|
||||||
|
cluster.forEach(tx => {
|
||||||
|
all.delete(tx.txid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// update survivors
|
||||||
|
all.forEach(tx => {
|
||||||
|
cluster.forEach(remove => {
|
||||||
|
if (tx.ancestors?.has(remove.txid)) {
|
||||||
|
// remove as dependency
|
||||||
|
tx.ancestors.delete(remove.txid);
|
||||||
|
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||||
|
// update ancestor sizes and fees
|
||||||
|
tx.ancestorsize -= remove.vsize;
|
||||||
|
tx.fees.ancestor -= remove.fees.base;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// recalculate fee rates
|
||||||
|
setAncestorScores(tx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||||
|
*
|
||||||
|
* @param tx
|
||||||
|
*/
|
||||||
|
export function setAncestorScores(tx: GraphTx): void {
|
||||||
|
tx.individualRate = tx.fees.base / tx.vsize;
|
||||||
|
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||||
|
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by descending score
|
||||||
|
export function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||||
|
return b.score - a.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||||
|
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||||
|
*/
|
||||||
|
export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||||
|
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||||
|
const mempoolArray: MinerTransaction[] = [];
|
||||||
|
|
||||||
|
candidates.forEach(tx => {
|
||||||
|
// initializing everything up front helps V8 optimize property access later
|
||||||
|
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||||
|
const feePerVsize = (tx.fee / adjustedVsize);
|
||||||
|
auditPool.set(tx.txid, {
|
||||||
|
txid: tx.txid,
|
||||||
|
order: txidToOrdering(tx.txid),
|
||||||
|
fee: tx.fee,
|
||||||
|
feeDelta: 0,
|
||||||
|
weight: tx.weight,
|
||||||
|
adjustedVsize,
|
||||||
|
feePerVsize: feePerVsize,
|
||||||
|
effectiveFeePerVsize: feePerVsize,
|
||||||
|
dependencyRate: feePerVsize,
|
||||||
|
sigops: tx.sigops || 0,
|
||||||
|
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||||
|
relativesSet: false,
|
||||||
|
ancestors: [],
|
||||||
|
cluster: [],
|
||||||
|
ancestorMap: new Map<string, MinerTransaction>(),
|
||||||
|
children: new Set<MinerTransaction>(),
|
||||||
|
ancestorFee: 0,
|
||||||
|
ancestorVsize: 0,
|
||||||
|
ancestorSigops: 0,
|
||||||
|
score: 0,
|
||||||
|
used: false,
|
||||||
|
modified: false,
|
||||||
|
});
|
||||||
|
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||||
|
});
|
||||||
|
|
||||||
|
// set accelerated effective fee
|
||||||
|
for (const acceleration of accelerations) {
|
||||||
|
const tx = auditPool.get(acceleration.txid);
|
||||||
|
if (tx) {
|
||||||
|
tx.feeDelta = acceleration.max_bid;
|
||||||
|
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||||
|
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||||
|
tx.dependencyRate = tx.feePerVsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build relatives graph & calculate ancestor scores
|
||||||
|
for (const tx of mempoolArray) {
|
||||||
|
if (!tx.relativesSet) {
|
||||||
|
setRelatives(tx, auditPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by descending ancestor score
|
||||||
|
mempoolArray.sort(priorityComparator);
|
||||||
|
|
||||||
|
// Build blocks by greedily choosing the highest feerate package
|
||||||
|
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||||
|
const blocks: number[][] = [];
|
||||||
|
let blockWeight = 0;
|
||||||
|
let blockSigops = 0;
|
||||||
|
const transactions: MinerTransaction[] = [];
|
||||||
|
let modified: MinerTransaction[] = [];
|
||||||
|
const overflow: MinerTransaction[] = [];
|
||||||
|
let failures = 0;
|
||||||
|
while (mempoolArray.length || modified.length) {
|
||||||
|
// skip invalid transactions
|
||||||
|
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
|
||||||
|
mempoolArray.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select best next package
|
||||||
|
let nextTx;
|
||||||
|
const nextPoolTx = mempoolArray[0];
|
||||||
|
const nextModifiedTx = modified[0];
|
||||||
|
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||||
|
nextTx = nextPoolTx;
|
||||||
|
mempoolArray.shift();
|
||||||
|
} else {
|
||||||
|
modified.shift();
|
||||||
|
if (nextModifiedTx) {
|
||||||
|
nextTx = nextModifiedTx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTx && !nextTx?.used) {
|
||||||
|
// Check if the package fits into this block
|
||||||
|
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||||
|
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
|
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||||
|
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||||
|
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||||
|
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||||
|
const used: MinerTransaction[] = [];
|
||||||
|
while (sortedTxSet.length) {
|
||||||
|
const ancestor = sortedTxSet.pop();
|
||||||
|
if (!ancestor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ancestor.used = true;
|
||||||
|
ancestor.usedBy = nextTx.txid;
|
||||||
|
// update this tx with effective fee rate & relatives data
|
||||||
|
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||||
|
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||||
|
}
|
||||||
|
ancestor.cluster = clusterTxids;
|
||||||
|
transactions.push(ancestor);
|
||||||
|
blockWeight += ancestor.weight;
|
||||||
|
blockSigops += ancestor.sigops;
|
||||||
|
used.push(ancestor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||||
|
if (used.length) {
|
||||||
|
used.forEach(tx => {
|
||||||
|
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
failures = 0;
|
||||||
|
} else {
|
||||||
|
// hold this package in an overflow list while we check for smaller options
|
||||||
|
overflow.push(nextTx);
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this block is full
|
||||||
|
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||||
|
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||||
|
|
||||||
|
if (exceededPackageTries || queueEmpty) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tx of transactions) {
|
||||||
|
tx.ancestors = Object.values(tx.ancestorMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// traverse in-mempool ancestors
|
||||||
|
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||||
|
function setRelatives(
|
||||||
|
tx: MinerTransaction,
|
||||||
|
mempool: Map<string, MinerTransaction>,
|
||||||
|
): void {
|
||||||
|
for (const parent of tx.inputs) {
|
||||||
|
const parentTx = mempool.get(parent);
|
||||||
|
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||||
|
tx.ancestorMap.set(parent, parentTx);
|
||||||
|
parentTx.children.add(tx);
|
||||||
|
// visit each node only once
|
||||||
|
if (!parentTx.relativesSet) {
|
||||||
|
setRelatives(parentTx, mempool);
|
||||||
|
}
|
||||||
|
parentTx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||||
|
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||||
|
tx.ancestorSigops = tx.sigops || 0;
|
||||||
|
tx.ancestorMap.forEach((ancestor) => {
|
||||||
|
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||||
|
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||||
|
tx.ancestorSigops += ancestor.sigops;
|
||||||
|
});
|
||||||
|
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||||
|
tx.relativesSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||||
|
// avoids recursion to limit call stack depth
|
||||||
|
function updateDescendants(
|
||||||
|
rootTx: MinerTransaction,
|
||||||
|
mempool: Map<string, MinerTransaction>,
|
||||||
|
modified: MinerTransaction[],
|
||||||
|
clusterRate: number,
|
||||||
|
): MinerTransaction[] {
|
||||||
|
const descendantSet: Set<MinerTransaction> = new Set();
|
||||||
|
// stack of nodes left to visit
|
||||||
|
const descendants: MinerTransaction[] = [];
|
||||||
|
let descendantTx: MinerTransaction | undefined;
|
||||||
|
rootTx.children.forEach(childTx => {
|
||||||
|
if (!descendantSet.has(childTx)) {
|
||||||
|
descendants.push(childTx);
|
||||||
|
descendantSet.add(childTx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
while (descendants.length) {
|
||||||
|
descendantTx = descendants.pop();
|
||||||
|
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||||
|
// remove tx as ancestor
|
||||||
|
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||||
|
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||||
|
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||||
|
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||||
|
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||||
|
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||||
|
|
||||||
|
if (!descendantTx.modified) {
|
||||||
|
descendantTx.modified = true;
|
||||||
|
modified.push(descendantTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add this node's children to the stack
|
||||||
|
descendantTx.children.forEach(childTx => {
|
||||||
|
// visit each node only once
|
||||||
|
if (!descendantSet.has(childTx)) {
|
||||||
|
descendants.push(childTx);
|
||||||
|
descendantSet.add(childTx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return new, resorted modified list
|
||||||
|
return modified.sort(priorityComparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||||
|
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||||
|
if (b.score === a.score) {
|
||||||
|
// tie-break by txid for stability
|
||||||
|
return a.order - b.order;
|
||||||
|
} else {
|
||||||
|
return b.score - a.score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the most significant 4 bytes of the txid as an integer
|
||||||
|
function txidToOrdering(txid: string): number {
|
||||||
|
return parseInt(
|
||||||
|
txid.substring(62, 64) +
|
||||||
|
txid.substring(60, 62) +
|
||||||
|
txid.substring(58, 60) +
|
||||||
|
txid.substring(56, 58),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import bitcoinClient from '../bitcoin/bitcoin-client';
|
|||||||
import mining from "./mining";
|
import mining from "./mining";
|
||||||
import PricesRepository from '../../repositories/PricesRepository';
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||||
|
import accelerationApi from '../services/acceleration';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class MiningRoutes {
|
class MiningRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@@ -24,6 +26,7 @@ class MiningRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees', this.$getBlockFeesTimespan)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||||
@@ -40,6 +43,8 @@ class MiningRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations)
|
||||||
|
.post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +54,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Prices are not available on testnets.');
|
handleError(req, res, 400, 'Prices are not available on testnets.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||||
const currency = req.query.currency as string;
|
const currency = req.query.currency as string;
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (timestamp && currency) {
|
if (timestamp && currency) {
|
||||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||||
@@ -67,7 +72,7 @@ class MiningRoutes {
|
|||||||
}
|
}
|
||||||
res.status(200).send(response);
|
res.status(200).send(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +85,9 @@ class MiningRoutes {
|
|||||||
res.json(stats);
|
res.json(stats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,9 +104,9 @@ class MiningRoutes {
|
|||||||
res.json(poolBlocks);
|
res.json(poolBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,7 @@ class MiningRoutes {
|
|||||||
res.json(pools);
|
res.json(pools);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +144,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(stats);
|
res.json(stats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +158,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(hashrates);
|
res.json(hashrates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,9 +173,9 @@ class MiningRoutes {
|
|||||||
res.json(hashrates);
|
res.json(hashrates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +204,7 @@ class MiningRoutes {
|
|||||||
currentDifficulty: currentDifficulty,
|
currentDifficulty: currentDifficulty,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +218,25 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockFees);
|
res.json(blockFees);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getBlockFeesTimespan(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
if (!parseInt(req.query.from as string, 10) || !parseInt(req.query.to as string, 10)) {
|
||||||
|
throw new Error('Invalid timestamp range');
|
||||||
|
}
|
||||||
|
if (parseInt(req.query.from as string, 10) > parseInt(req.query.to as string, 10)) {
|
||||||
|
throw new Error('from must be less than to');
|
||||||
|
}
|
||||||
|
const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10));
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(blockFees);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +250,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockRewards);
|
res.json(blockRewards);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +264,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blockFeeRates);
|
res.json(blockFeeRates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +282,7 @@ class MiningRoutes {
|
|||||||
weights: blockWeights
|
weights: blockWeights
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +294,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +318,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +327,7 @@ class MiningRoutes {
|
|||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||||
|
|
||||||
if (!audit) {
|
if (!audit) {
|
||||||
res.status(204).send(`This block has not been audited.`);
|
handleError(req, res, 204, `This block has not been audited.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +336,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
res.json(audit);
|
res.json(audit);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +359,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +372,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +385,7 @@ class MiningRoutes {
|
|||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
res.json(audit || 'null');
|
res.json(audit || 'null');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,12 +395,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,13 +410,13 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,12 +426,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,12 +441,39 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getActiveAccelerations(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).send(accelerationApi.accelerations || []);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
||||||
|
res.setHeader('expires', -1);
|
||||||
|
try {
|
||||||
|
accelerationApi.accelerationRequested(req.params.txid);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,22 @@ class Mining {
|
|||||||
*/
|
*/
|
||||||
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
|
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
|
||||||
return await BlocksRepository.$getHistoricalBlockFees(
|
return await BlocksRepository.$getHistoricalBlockFees(
|
||||||
this.getTimeRange(interval, 5),
|
this.getTimeRange(interval),
|
||||||
Common.getSqlInterval(interval)
|
Common.getSqlInterval(interval)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get timespan block total fees
|
||||||
|
*/
|
||||||
|
public async $getBlockFeesTimespan(from: number, to: number): Promise<number> {
|
||||||
|
return await BlocksRepository.$getHistoricalBlockFees(
|
||||||
|
this.getTimeRangeFromTimespan(from, to),
|
||||||
|
null,
|
||||||
|
{from, to}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get historical block rewards
|
* Get historical block rewards
|
||||||
*/
|
*/
|
||||||
@@ -646,6 +657,24 @@ class Mining {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTimeRangeFromTimespan(from: number, to: number, scale = 1): number {
|
||||||
|
const timespan = to - from;
|
||||||
|
switch (true) {
|
||||||
|
case timespan > 3600 * 24 * 365 * 4: return 86400 * scale; // 24h
|
||||||
|
case timespan > 3600 * 24 * 365 * 3: return 43200 * scale; // 12h
|
||||||
|
case timespan > 3600 * 24 * 365 * 2: return 43200 * scale; // 12h
|
||||||
|
case timespan > 3600 * 24 * 365: return 28800 * scale; // 8h
|
||||||
|
case timespan > 3600 * 24 * 30 * 6: return 28800 * scale; // 8h
|
||||||
|
case timespan > 3600 * 24 * 30 * 3: return 10800 * scale; // 3h
|
||||||
|
case timespan > 3600 * 24 * 30: return 7200 * scale; // 2h
|
||||||
|
case timespan > 3600 * 24 * 7: return 1800 * scale; // 30min
|
||||||
|
case timespan > 3600 * 24 * 3: return 300 * scale; // 5min
|
||||||
|
case timespan > 3600 * 24: return 1 * scale;
|
||||||
|
default: return 1 * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Finds the oldest block in a consecutive chain back from the tip
|
// Finds the oldest block in a consecutive chain back from the tip
|
||||||
// assumes `blocks` is sorted in ascending height order
|
// assumes `blocks` is sorted in ascending height order
|
||||||
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
|
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import PoolsRepository from '../repositories/PoolsRepository';
|
|||||||
import { PoolTag } from '../mempool.interfaces';
|
import { PoolTag } from '../mempool.interfaces';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
|
import redisCache from './redis-cache';
|
||||||
|
|
||||||
class PoolsParser {
|
class PoolsParser {
|
||||||
miningPools: any[] = [];
|
miningPools: any[] = [];
|
||||||
@@ -37,28 +40,53 @@ class PoolsParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate our db with updated mining pool definition
|
* Populate our db with updated mining pool definition
|
||||||
* @param pools
|
* @param pools
|
||||||
*/
|
*/
|
||||||
public async migratePoolsJson(): Promise<void> {
|
public async migratePoolsJson(): Promise<void> {
|
||||||
// We also need to wipe the backend cache to make sure we don't serve blocks with
|
// We also need to wipe the backend cache to make sure we don't serve blocks with
|
||||||
// the wrong mining pool (usually happen with unknown blocks)
|
// the wrong mining pool (usually happen with unknown blocks)
|
||||||
diskCache.setIgnoreBlocksCache();
|
diskCache.setIgnoreBlocksCache();
|
||||||
|
redisCache.setIgnoreBlocksCache();
|
||||||
|
|
||||||
await this.$insertUnknownPool();
|
await this.$insertUnknownPool();
|
||||||
|
|
||||||
|
let reindexUnknown = false;
|
||||||
|
|
||||||
for (const pool of this.miningPools) {
|
for (const pool of this.miningPools) {
|
||||||
if (!pool.id) {
|
if (!pool.id) {
|
||||||
logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`);
|
logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One of the two fields 'addresses' or 'regexes' must be a non-empty array
|
||||||
|
if (!pool.addresses && !pool.regexes) {
|
||||||
|
logger.err(`Mining pool ${pool.name} must have at least one of the fields 'addresses' or 'regexes'. Skipping.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.addresses = pool.addresses || [];
|
||||||
|
pool.regexes = pool.regexes || [];
|
||||||
|
|
||||||
|
if (pool.addresses.length === 0 && pool.regexes.length === 0) {
|
||||||
|
logger.err(`Mining pool ${pool.name} has no 'addresses' nor 'regexes' defined. Skipping.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool.addresses.length === 0) {
|
||||||
|
logger.warn(`Mining pool ${pool.name} has no 'addresses' defined.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool.regexes.length === 0) {
|
||||||
|
logger.warn(`Mining pool ${pool.name} has no 'regexes' defined.`);
|
||||||
|
}
|
||||||
|
|
||||||
const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false);
|
const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false);
|
||||||
if (!poolDB) {
|
if (!poolDB) {
|
||||||
// New mining pool
|
// New mining pool
|
||||||
const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
||||||
logger.debug(`Inserting new mining pool ${pool.name}`);
|
logger.debug(`Inserting new mining pool ${pool.name}`);
|
||||||
await PoolsRepository.$insertNewMiningPool(pool, slug);
|
await PoolsRepository.$insertNewMiningPool(pool, slug);
|
||||||
await this.$deleteUnknownBlocks();
|
reindexUnknown = true;
|
||||||
} else {
|
} else {
|
||||||
if (poolDB.name !== pool.name) {
|
if (poolDB.name !== pool.name) {
|
||||||
// Pool has been renamed
|
// Pool has been renamed
|
||||||
@@ -76,7 +104,45 @@ class PoolsParser {
|
|||||||
// Pool addresses changed or coinbase tags changed
|
// Pool addresses changed or coinbase tags changed
|
||||||
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
|
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
|
||||||
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
|
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
|
||||||
await this.$deleteBlocksForPool(poolDB);
|
reindexUnknown = true;
|
||||||
|
await this.$reindexBlocksForPool(poolDB.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reindexUnknown) {
|
||||||
|
logger.notice(`Updating addresses and/or coinbase tags for unknown mining pool.`);
|
||||||
|
let unknownPool;
|
||||||
|
if (config.DATABASE.ENABLED === true) {
|
||||||
|
unknownPool = await PoolsRepository.$getUnknownPool();
|
||||||
|
} else {
|
||||||
|
unknownPool = this.unknownPool;
|
||||||
|
}
|
||||||
|
await this.$reindexBlocksForPool(unknownPool.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined {
|
||||||
|
const asciiScriptSig = transactionUtils.hex2ascii(scriptsig);
|
||||||
|
|
||||||
|
for (let i = 0; i < pools.length; ++i) {
|
||||||
|
if (addresses.length) {
|
||||||
|
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
|
||||||
|
JSON.parse(pools[i].addresses) : pools[i].addresses;
|
||||||
|
for (let y = 0; y < poolAddresses.length; y++) {
|
||||||
|
if (addresses.indexOf(poolAddresses[y]) !== -1) {
|
||||||
|
return pools[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexes: string[] = typeof pools[i].regexes === 'string' ?
|
||||||
|
JSON.parse(pools[i].regexes) : pools[i].regexes;
|
||||||
|
for (let y = 0; y < regexes.length; ++y) {
|
||||||
|
const regex = new RegExp(regexes[y], 'i');
|
||||||
|
const match = asciiScriptSig.match(regex);
|
||||||
|
if (match !== null) {
|
||||||
|
return pools[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,68 +178,47 @@ class PoolsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete indexed blocks for an updated mining pool
|
* re-index pool assignment for blocks previously associated with pool
|
||||||
*
|
*
|
||||||
* @param pool
|
* @param pool local id of existing pool to reindex
|
||||||
*/
|
*/
|
||||||
private async $deleteBlocksForPool(pool: PoolTag): Promise<void> {
|
private async $reindexBlocksForPool(poolId: number): Promise<void> {
|
||||||
// Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
|
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
|
||||||
// Ignore early days of Bitcoin as there were no mining pool yet
|
if (config.MEMPOOL.NETWORK === 'testnet') {
|
||||||
const [oldestPoolBlock]: any[] = await DB.query(`
|
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
|
||||||
SELECT height
|
} else if (config.MEMPOOL.NETWORK === 'signet') {
|
||||||
|
firstKnownBlockPool = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [blocks]: any[] = await DB.query(`
|
||||||
|
SELECT height, hash, coinbase_raw, coinbase_addresses
|
||||||
FROM blocks
|
FROM blocks
|
||||||
WHERE pool_id = ?
|
WHERE pool_id = ?
|
||||||
ORDER BY height
|
AND height >= ?
|
||||||
LIMIT 1`,
|
ORDER BY height DESC
|
||||||
[pool.id]
|
`, [poolId, firstKnownBlockPool]);
|
||||||
);
|
|
||||||
|
|
||||||
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
|
let pools: PoolTag[] = [];
|
||||||
if (config.MEMPOOL.NETWORK === 'testnet') {
|
if (config.DATABASE.ENABLED === true) {
|
||||||
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
|
pools = await PoolsRepository.$getPools();
|
||||||
} else if (config.MEMPOOL.NETWORK === 'signet') {
|
} else {
|
||||||
firstKnownBlockPool = 0;
|
pools = this.miningPools;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : firstKnownBlockPool;
|
let changed = 0;
|
||||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
for (const block of blocks) {
|
||||||
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`);
|
const addresses = JSON.parse(block.coinbase_addresses) || [];
|
||||||
await DB.query(`
|
const newPool = this.matchBlockMiner(block.coinbase_raw, addresses, pools);
|
||||||
DELETE FROM blocks
|
if (newPool && newPool.id !== poolId) {
|
||||||
WHERE pool_id = ? AND height >= ${oldestBlockHeight}`,
|
changed++;
|
||||||
[unknownPool[0].id]
|
await BlocksRepository.$savePool(block.hash, newPool.id);
|
||||||
);
|
}
|
||||||
logger.notice(`Deleting blocks from ${pool.name} mining pool for re-indexing`);
|
}
|
||||||
await DB.query(`
|
|
||||||
DELETE FROM blocks
|
logger.info(`${changed} blocks assigned to a new pool`, logger.tags.mining);
|
||||||
WHERE pool_id = ?`,
|
|
||||||
[pool.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-index hashrates and difficulty adjustments later
|
// Re-index hashrates and difficulty adjustments later
|
||||||
mining.reindexHashrateRequested = true;
|
mining.reindexHashrateRequested = true;
|
||||||
mining.reindexDifficultyAdjustmentRequested = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $deleteUnknownBlocks(): Promise<void> {
|
|
||||||
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
|
|
||||||
if (config.MEMPOOL.NETWORK === 'testnet') {
|
|
||||||
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
|
|
||||||
} else if (config.MEMPOOL.NETWORK === 'signet') {
|
|
||||||
firstKnownBlockPool = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
|
||||||
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${firstKnownBlockPool} for re-indexing`);
|
|
||||||
await DB.query(`
|
|
||||||
DELETE FROM blocks
|
|
||||||
WHERE pool_id = ? AND height >= ${firstKnownBlockPool}`,
|
|
||||||
[unknownPool[0].id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-index hashrates and difficulty adjustments later
|
|
||||||
mining.reindexHashrateRequested = true;
|
|
||||||
mining.reindexDifficultyAdjustmentRequested = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ interface CacheEvent {
|
|||||||
value?: any,
|
value?: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton for tracking RBF trees
|
||||||
|
*
|
||||||
|
* Maintains a set of RBF trees, where each tree represents a sequence of
|
||||||
|
* consecutive RBF replacements.
|
||||||
|
*
|
||||||
|
* Trees are identified by the txid of the root transaction.
|
||||||
|
*
|
||||||
|
* To maintain consistency, the following invariants must be upheld:
|
||||||
|
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
||||||
|
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
||||||
|
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
||||||
|
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
||||||
|
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
||||||
|
*/
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private replacedBy: Map<string, string> = new Map();
|
private replacedBy: Map<string, string> = new Map();
|
||||||
private replaces: Map<string, string[]> = new Map();
|
private replaces: Map<string, string[]> = new Map();
|
||||||
@@ -61,6 +77,10 @@ class RbfCache {
|
|||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level cache operations
|
||||||
|
*/
|
||||||
|
|
||||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||||
this.txs.set(txid, tx);
|
this.txs.set(txid, tx);
|
||||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||||
@@ -92,6 +112,12 @@ class RbfCache {
|
|||||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic data structure operations
|
||||||
|
* must uphold tree invariants
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||||
return;
|
return;
|
||||||
@@ -114,6 +140,10 @@ class RbfCache {
|
|||||||
if (!replacedTx.rbf) {
|
if (!replacedTx.rbf) {
|
||||||
txFullRbf = true;
|
txFullRbf = true;
|
||||||
}
|
}
|
||||||
|
if (this.replacedBy.has(replacedTx.txid)) {
|
||||||
|
// should never happen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||||
if (this.treeMap.has(replacedTx.txid)) {
|
if (this.treeMap.has(replacedTx.txid)) {
|
||||||
const treeId = this.treeMap.get(replacedTx.txid);
|
const treeId = this.treeMap.get(replacedTx.txid);
|
||||||
@@ -140,18 +170,47 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
newTx.fullRbf = txFullRbf;
|
newTx.fullRbf = txFullRbf;
|
||||||
const treeId = replacedTrees[0].tx.txid;
|
|
||||||
const newTree = {
|
const newTree = {
|
||||||
tx: newTx,
|
tx: newTx,
|
||||||
time: newTime,
|
time: newTime,
|
||||||
fullRbf: treeFullRbf,
|
fullRbf: treeFullRbf,
|
||||||
replaces: replacedTrees
|
replaces: replacedTrees
|
||||||
};
|
};
|
||||||
this.addTree(treeId, newTree);
|
this.addTree(newTree.tx.txid, newTree);
|
||||||
this.updateTreeMap(treeId, newTree);
|
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public mined(txid): void {
|
||||||
|
if (!this.txs.has(txid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const treeId = this.treeMap.get(txid);
|
||||||
|
if (treeId && this.rbfTrees.has(treeId)) {
|
||||||
|
const tree = this.rbfTrees.get(treeId);
|
||||||
|
if (tree) {
|
||||||
|
this.setTreeMined(tree, txid);
|
||||||
|
tree.mined = true;
|
||||||
|
this.dirtyTrees.add(treeId);
|
||||||
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.evict(txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag a transaction as removed from the mempool
|
||||||
|
public evict(txid: string, fast: boolean = false): void {
|
||||||
|
this.evictionCount++;
|
||||||
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||||
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||||
|
this.addExpiration(txid, expiryTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only public interface
|
||||||
|
*/
|
||||||
|
|
||||||
public has(txId: string): boolean {
|
public has(txId: string): boolean {
|
||||||
return this.txs.has(txId);
|
return this.txs.has(txId);
|
||||||
}
|
}
|
||||||
@@ -232,32 +291,6 @@ class RbfCache {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mined(txid): void {
|
|
||||||
if (!this.txs.has(txid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const treeId = this.treeMap.get(txid);
|
|
||||||
if (treeId && this.rbfTrees.has(treeId)) {
|
|
||||||
const tree = this.rbfTrees.get(treeId);
|
|
||||||
if (tree) {
|
|
||||||
this.setTreeMined(tree, txid);
|
|
||||||
tree.mined = true;
|
|
||||||
this.dirtyTrees.add(treeId);
|
|
||||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.evict(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// flag a transaction as removed from the mempool
|
|
||||||
public evict(txid: string, fast: boolean = false): void {
|
|
||||||
this.evictionCount++;
|
|
||||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
|
||||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
|
||||||
this.addExpiration(txid, expiryTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the transaction involved in a full rbf replacement?
|
// is the transaction involved in a full rbf replacement?
|
||||||
public isFullRbf(txid: string): boolean {
|
public isFullRbf(txid: string): boolean {
|
||||||
const treeId = this.treeMap.get(txid);
|
const treeId = this.treeMap.get(txid);
|
||||||
@@ -271,6 +304,10 @@ class RbfCache {
|
|||||||
return tree?.fullRbf;
|
return tree?.fullRbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache maintenance & utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const txid of this.expiring.keys()) {
|
for (const txid of this.expiring.keys()) {
|
||||||
@@ -299,10 +336,6 @@ class RbfCache {
|
|||||||
for (const tx of (replaces || [])) {
|
for (const tx of (replaces || [])) {
|
||||||
// recursively remove prior versions from the cache
|
// recursively remove prior versions from the cache
|
||||||
this.replacedBy.delete(tx);
|
this.replacedBy.delete(tx);
|
||||||
// if this is the id of a tree, remove that too
|
|
||||||
if (this.treeMap.get(tx) === tx) {
|
|
||||||
this.removeTree(tx);
|
|
||||||
}
|
|
||||||
this.remove(tx);
|
this.remove(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,14 +403,21 @@ class RbfCache {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async load({ txs, trees, expiring, mempool }): Promise<void> {
|
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
|
||||||
try {
|
try {
|
||||||
txs.forEach(txEntry => {
|
txs.forEach(txEntry => {
|
||||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
for (const deflatedTree of trees) {
|
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||||
|
if (tree) {
|
||||||
|
this.addTree(tree.tx.txid, tree);
|
||||||
|
this.updateTreeMap(tree.tx.txid, tree);
|
||||||
|
if (tree.mined) {
|
||||||
|
this.evict(tree.tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expiring.forEach(expiringEntry => {
|
expiring.forEach(expiringEntry => {
|
||||||
if (this.txs.has(expiringEntry.key)) {
|
if (this.txs.has(expiringEntry.key)) {
|
||||||
@@ -385,6 +425,31 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
|
|
||||||
|
// connect cached trees to current mempool transactions
|
||||||
|
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
|
||||||
|
for (const tree of this.rbfTrees.values()) {
|
||||||
|
const tx = this.getTx(tree.tx.txid);
|
||||||
|
if (!tx || tree.mined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||||
|
if (conflict && conflict.txid !== tx.txid) {
|
||||||
|
if (!conflicts[conflict.txid]) {
|
||||||
|
conflicts[conflict.txid] = {
|
||||||
|
replacedBy: conflict,
|
||||||
|
replaces: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
conflicts[conflict.txid].replaces.add(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const { replacedBy, replaces } of Object.values(conflicts)) {
|
||||||
|
this.add([...replaces.values()], replacedBy);
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkTrees();
|
await this.checkTrees();
|
||||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@@ -426,6 +491,12 @@ class RbfCache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if this tx is already in the cache, return early
|
||||||
|
if (this.treeMap.has(txid)) {
|
||||||
|
this.removeTree(deflated.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// recursively reconstruct child trees
|
// recursively reconstruct child trees
|
||||||
for (const childId of treeInfo.replaces) {
|
for (const childId of treeInfo.replaces) {
|
||||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||||
@@ -457,10 +528,6 @@ class RbfCache {
|
|||||||
fullRbf: treeInfo.fullRbf,
|
fullRbf: treeInfo.fullRbf,
|
||||||
replaces,
|
replaces,
|
||||||
};
|
};
|
||||||
this.treeMap.set(txid, root);
|
|
||||||
if (root === txid) {
|
|
||||||
this.addTree(root, tree);
|
|
||||||
}
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +578,7 @@ class RbfCache {
|
|||||||
processTxs(txs);
|
processTxs(txs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evict missing transactions
|
||||||
for (const txid of txids) {
|
for (const txid of txids) {
|
||||||
if (!found[txid]) {
|
if (!found[txid]) {
|
||||||
this.evict(txid, false);
|
this.evict(txid, false);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class RedisCache {
|
|||||||
private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
|
private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
|
||||||
private rbfRemoveQueue: { type: string, txid: string }[] = [];
|
private rbfRemoveQueue: { type: string, txid: string }[] = [];
|
||||||
private txFlushLimit: number = 10000;
|
private txFlushLimit: number = 10000;
|
||||||
|
private ignoreBlocksCache = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (config.REDIS.ENABLED) {
|
if (config.REDIS.ENABLED) {
|
||||||
@@ -155,7 +156,7 @@ class RedisCache {
|
|||||||
const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
|
const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
|
||||||
try {
|
try {
|
||||||
const msetData = toAdd.map(tx => {
|
const msetData = toAdd.map(tx => {
|
||||||
const minified: any = { ...tx };
|
const minified: any = structuredClone(tx);
|
||||||
delete minified.hex;
|
delete minified.hex;
|
||||||
for (const vin of minified.vin) {
|
for (const vin of minified.vin) {
|
||||||
delete vin.inner_redeemscript_asm;
|
delete vin.inner_redeemscript_asm;
|
||||||
@@ -341,9 +342,7 @@ class RedisCache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info('Restoring mempool and blocks data from Redis cache');
|
logger.info('Restoring mempool and blocks data from Redis cache');
|
||||||
// Load block data
|
|
||||||
const loadedBlocks = await this.$getBlocks();
|
|
||||||
const loadedBlockSummaries = await this.$getBlockSummaries();
|
|
||||||
// Load mempool
|
// Load mempool
|
||||||
const loadedMempool = await this.$getMempool();
|
const loadedMempool = await this.$getMempool();
|
||||||
this.inflateLoadedTxs(loadedMempool);
|
this.inflateLoadedTxs(loadedMempool);
|
||||||
@@ -352,15 +351,21 @@ class RedisCache {
|
|||||||
const rbfTrees = await this.$getRbfEntries('tree');
|
const rbfTrees = await this.$getRbfEntries('tree');
|
||||||
const rbfExpirations = await this.$getRbfEntries('exp');
|
const rbfExpirations = await this.$getRbfEntries('exp');
|
||||||
|
|
||||||
// Set loaded data
|
// Load & set block data
|
||||||
blocks.setBlocks(loadedBlocks || []);
|
if (!this.ignoreBlocksCache) {
|
||||||
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
const loadedBlocks = await this.$getBlocks();
|
||||||
|
const loadedBlockSummaries = await this.$getBlockSummaries();
|
||||||
|
blocks.setBlocks(loadedBlocks || []);
|
||||||
|
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
||||||
|
}
|
||||||
|
// Set other data
|
||||||
await memPool.$setMempool(loadedMempool);
|
await memPool.$setMempool(loadedMempool);
|
||||||
await rbfCache.load({
|
await rbfCache.load({
|
||||||
txs: rbfTxs,
|
txs: rbfTxs,
|
||||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||||
expiring: rbfExpirations,
|
expiring: rbfExpirations,
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +416,10 @@ class RedisCache {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setIgnoreBlocksCache(): void {
|
||||||
|
this.ignoreBlocksCache = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new RedisCache();
|
export default new RedisCache();
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
import { BlockExtended } from '../../mempool.interfaces';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||||
|
|
||||||
export interface Acceleration {
|
export interface Acceleration {
|
||||||
txid: string,
|
txid: string,
|
||||||
|
added: number,
|
||||||
|
effectiveVsize: number,
|
||||||
|
effectiveFee: number,
|
||||||
feeDelta: number,
|
feeDelta: number,
|
||||||
pools: number[],
|
pools: number[],
|
||||||
|
positions?: {
|
||||||
|
[pool: number]: {
|
||||||
|
block: number,
|
||||||
|
vbytes: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AccelerationHistory {
|
export interface AccelerationHistory {
|
||||||
@@ -22,25 +33,95 @@ export interface AccelerationHistory {
|
|||||||
feeDelta: number,
|
feeDelta: number,
|
||||||
blockHash: string,
|
blockHash: string,
|
||||||
blockHeight: number,
|
blockHeight: number,
|
||||||
pools: {
|
pools: number[];
|
||||||
pool_unique_id: number,
|
|
||||||
username: string,
|
|
||||||
}[],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AccelerationApi {
|
class AccelerationApi {
|
||||||
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||||
try {
|
private _accelerations: Acceleration[] | null = null;
|
||||||
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 });
|
private lastPoll = 0;
|
||||||
return response.data as Acceleration[];
|
private forcePoll = false;
|
||||||
} catch (e) {
|
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||||
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
|
||||||
return null;
|
public get accelerations(): Acceleration[] | null {
|
||||||
|
return this._accelerations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number {
|
||||||
|
return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public accelerationRequested(txid: string): void {
|
||||||
|
if (this.onDemandPollingEnabled) {
|
||||||
|
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public accelerationConfirmed(): void {
|
||||||
|
this.forcePoll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
|
||||||
|
return response?.data || [];
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||||
|
if (!this.onDemandPollingEnabled) {
|
||||||
|
const accelerations = await this.$fetchAccelerations();
|
||||||
|
if (accelerations) {
|
||||||
|
this._accelerations = accelerations;
|
||||||
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return this.$updateAccelerationsOnDemand();
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||||
|
const shouldUpdate = this.forcePoll
|
||||||
|
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||||
|
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||||
|
|
||||||
|
// update accelerations if necessary
|
||||||
|
if (shouldUpdate) {
|
||||||
|
const accelerations = await this.$fetchAccelerations();
|
||||||
|
this.lastPoll = Date.now();
|
||||||
|
this.forcePoll = false;
|
||||||
|
if (accelerations) {
|
||||||
|
const latestAccelerations: Record<string, Acceleration> = {};
|
||||||
|
// set relevant accelerations to 'accelerating'
|
||||||
|
for (const acc of accelerations) {
|
||||||
|
if (this.myAccelerations[acc.txid]) {
|
||||||
|
latestAccelerations[acc.txid] = acc;
|
||||||
|
this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
|
||||||
|
for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) {
|
||||||
|
if (status === 'accelerating' && !latestAccelerations[txid]) {
|
||||||
|
this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
|
||||||
|
for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) {
|
||||||
|
if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) {
|
||||||
|
delete this.myAccelerations[txid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||||
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
|
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class StatisticsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $create(statistics: Statistic): Promise<number | undefined> {
|
public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> {
|
||||||
try {
|
try {
|
||||||
const query = `INSERT INTO statistics(
|
const query = `INSERT INTO statistics(
|
||||||
added,
|
added,
|
||||||
@@ -114,7 +114,7 @@ class StatisticsApi {
|
|||||||
vsize_1800,
|
vsize_1800,
|
||||||
vsize_2000
|
vsize_2000
|
||||||
)
|
)
|
||||||
VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
const params: (string | number)[] = [
|
const params: (string | number)[] = [
|
||||||
@@ -456,6 +456,59 @@ class StatisticsApi {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public mapOptimizedStatisticToStatistic(statistic: OptimizedStatistic[]): Statistic[] {
|
||||||
|
return statistic.map((s) => {
|
||||||
|
return {
|
||||||
|
added: s.added,
|
||||||
|
unconfirmed_transactions: s.count,
|
||||||
|
tx_per_second: 0,
|
||||||
|
vbytes_per_second: s.vbytes_per_second,
|
||||||
|
mempool_byte_weight: s.mempool_byte_weight || 0,
|
||||||
|
total_fee: s.total_fee || 0,
|
||||||
|
min_fee: s.min_fee,
|
||||||
|
fee_data: '',
|
||||||
|
vsize_1: s.vsizes[0],
|
||||||
|
vsize_2: s.vsizes[1],
|
||||||
|
vsize_3: s.vsizes[2],
|
||||||
|
vsize_4: s.vsizes[3],
|
||||||
|
vsize_5: s.vsizes[4],
|
||||||
|
vsize_6: s.vsizes[5],
|
||||||
|
vsize_8: s.vsizes[6],
|
||||||
|
vsize_10: s.vsizes[7],
|
||||||
|
vsize_12: s.vsizes[8],
|
||||||
|
vsize_15: s.vsizes[9],
|
||||||
|
vsize_20: s.vsizes[10],
|
||||||
|
vsize_30: s.vsizes[11],
|
||||||
|
vsize_40: s.vsizes[12],
|
||||||
|
vsize_50: s.vsizes[13],
|
||||||
|
vsize_60: s.vsizes[14],
|
||||||
|
vsize_70: s.vsizes[15],
|
||||||
|
vsize_80: s.vsizes[16],
|
||||||
|
vsize_90: s.vsizes[17],
|
||||||
|
vsize_100: s.vsizes[18],
|
||||||
|
vsize_125: s.vsizes[19],
|
||||||
|
vsize_150: s.vsizes[20],
|
||||||
|
vsize_175: s.vsizes[21],
|
||||||
|
vsize_200: s.vsizes[22],
|
||||||
|
vsize_250: s.vsizes[23],
|
||||||
|
vsize_300: s.vsizes[24],
|
||||||
|
vsize_350: s.vsizes[25],
|
||||||
|
vsize_400: s.vsizes[26],
|
||||||
|
vsize_500: s.vsizes[27],
|
||||||
|
vsize_600: s.vsizes[28],
|
||||||
|
vsize_700: s.vsizes[29],
|
||||||
|
vsize_800: s.vsizes[30],
|
||||||
|
vsize_900: s.vsizes[31],
|
||||||
|
vsize_1000: s.vsizes[32],
|
||||||
|
vsize_1200: s.vsizes[33],
|
||||||
|
vsize_1400: s.vsizes[34],
|
||||||
|
vsize_1600: s.vsizes[35],
|
||||||
|
vsize_1800: s.vsizes[36],
|
||||||
|
vsize_2000: s.vsizes[37],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new StatisticsApi();
|
export default new StatisticsApi();
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class TransactionUtils {
|
|||||||
}
|
}
|
||||||
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
||||||
const transactionExtended: TransactionExtended = Object.assign({
|
const transactionExtended: TransactionExtended = Object.assign({
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize: transaction.weight / 4,
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
effectiveFeePerVsize: feePerVbytes,
|
effectiveFeePerVsize: feePerVbytes,
|
||||||
}, transaction);
|
}, transaction);
|
||||||
@@ -123,7 +123,7 @@ class TransactionUtils {
|
|||||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
order: this.txidToOrdering(transaction.txid),
|
order: this.txidToOrdering(transaction.txid),
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize,
|
||||||
adjustedVsize,
|
adjustedVsize,
|
||||||
sigops,
|
sigops,
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
@@ -338,6 +338,87 @@ class TransactionUtils {
|
|||||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||||
return witness[positionOfScript];
|
return witness[positionOfScript];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||||
|
// (i.e. the most likely prioritizations and deprioritizations)
|
||||||
|
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||||
|
// find the longest increasing subsequence of transactions
|
||||||
|
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||||
|
// should be O(n log n)
|
||||||
|
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||||
|
if (X.length < 2) {
|
||||||
|
return { prioritized: [], deprioritized: [] };
|
||||||
|
}
|
||||||
|
const N = X.length;
|
||||||
|
const P: number[] = new Array(N);
|
||||||
|
const M: number[] = new Array(N + 1);
|
||||||
|
M[0] = -1; // undefined so can be set to any value
|
||||||
|
|
||||||
|
let L = 0;
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
// Binary search for the smallest positive l ≤ L
|
||||||
|
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||||
|
let lo = 1;
|
||||||
|
let hi = L + 1;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||||
|
if (X[M[mid]].rate > X[i].rate) {
|
||||||
|
hi = mid;
|
||||||
|
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After searching, lo == hi is 1 greater than the
|
||||||
|
// length of the longest prefix of X[i]
|
||||||
|
const newL = lo;
|
||||||
|
|
||||||
|
// The predecessor of X[i] is the last index of
|
||||||
|
// the subsequence of length newL-1
|
||||||
|
P[i] = M[newL - 1];
|
||||||
|
M[newL] = i;
|
||||||
|
|
||||||
|
if (newL > L) {
|
||||||
|
// If we found a subsequence longer than any we've
|
||||||
|
// found yet, update L
|
||||||
|
L = newL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the longest increasing subsequence
|
||||||
|
// It consists of the values of X at the L indices:
|
||||||
|
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||||
|
const LIS: any[] = new Array(L);
|
||||||
|
let k = M[L];
|
||||||
|
for (let j = L - 1; j >= 0; j--) {
|
||||||
|
LIS[j] = X[k];
|
||||||
|
k = P[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lisMap = new Map<string, number>();
|
||||||
|
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||||
|
|
||||||
|
const prioritized: string[] = [];
|
||||||
|
const deprioritized: string[] = [];
|
||||||
|
|
||||||
|
let lastRate = X[0].rate;
|
||||||
|
|
||||||
|
for (const tx of X) {
|
||||||
|
if (lisMap.has(tx.txid)) {
|
||||||
|
lastRate = tx.rate;
|
||||||
|
} else {
|
||||||
|
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||||
|
// skip if the rate is almost the same as the previous transaction
|
||||||
|
} else if (tx.rate <= lastRate) {
|
||||||
|
prioritized.push(tx.txid);
|
||||||
|
} else {
|
||||||
|
deprioritized.push(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { prioritized, deprioritized };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TransactionUtils();
|
export default new TransactionUtils();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
|
|||||||
import {
|
import {
|
||||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||||
|
MempoolDelta, MempoolDeltaTxids
|
||||||
} from '../mempool.interfaces';
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
@@ -32,7 +33,7 @@ interface AddressTransactions {
|
|||||||
removed: MempoolTransactionExtended[],
|
removed: MempoolTransactionExtended[],
|
||||||
}
|
}
|
||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import { calculateCpfp } from './cpfp';
|
import { calculateMempoolTxCpfp } from './cpfp';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@@ -44,7 +45,7 @@ const wantable = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private webSocketServers: WebSocket.Server[] = [];
|
||||||
private extraInitProperties = {};
|
private extraInitProperties = {};
|
||||||
|
|
||||||
private numClients = 0;
|
private numClients = 0;
|
||||||
@@ -54,11 +55,12 @@ class WebsocketHandler {
|
|||||||
private socketData: { [key: string]: string } = {};
|
private socketData: { [key: string]: string } = {};
|
||||||
private serializedInitData: string = '{}';
|
private serializedInitData: string = '{}';
|
||||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||||
|
private mempoolSequence: number = 0;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
setWebsocketServer(wss: WebSocket.Server) {
|
addWebsocketServer(wss: WebSocket.Server) {
|
||||||
this.wss = wss;
|
this.webSocketServers.push(wss);
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtraInitData(property: string, value: any) {
|
setExtraInitData(property: string, value: any) {
|
||||||
@@ -102,11 +104,13 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConnectionHandling() {
|
setupConnectionHandling() {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.on('connection', (client: WebSocket, req) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.on('connection', (client: WebSocket, req) => {
|
||||||
this.numConnected++;
|
this.numConnected++;
|
||||||
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
|
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
|
||||||
client.on('error', (e) => {
|
client.on('error', (e) => {
|
||||||
@@ -202,7 +206,8 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
response['txPosition'] = JSON.stringify({
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position
|
position,
|
||||||
|
accelerationPositions: memPool.getAccelerationPositions(tx.txid),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -315,6 +320,7 @@ class WebsocketHandler {
|
|||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
response['projected-block-transactions'] = JSON.stringify({
|
response['projected-block-transactions'] = JSON.stringify({
|
||||||
index: index,
|
index: index,
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -342,6 +348,17 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
|
||||||
|
if (parsedMessage['track-accelerations']) {
|
||||||
|
client['track-accelerations'] = true;
|
||||||
|
response['accelerations'] = JSON.stringify({
|
||||||
|
accelerations: Object.values(memPool.getAccelerations()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client['track-accelerations'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
|
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
|
||||||
this.updateSocketData();
|
this.updateSocketData();
|
||||||
@@ -360,6 +377,18 @@ class WebsocketHandler {
|
|||||||
client['track-donation'] = parsedMessage['track-donation'];
|
client['track-donation'] = parsedMessage['track-donation'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage['track-mempool-txids'] === true) {
|
||||||
|
client['track-mempool-txids'] = true;
|
||||||
|
} else if (parsedMessage['track-mempool-txids'] === false) {
|
||||||
|
delete client['track-mempool-txids'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedMessage['track-mempool'] === true) {
|
||||||
|
client['track-mempool'] = true;
|
||||||
|
} else if (parsedMessage['track-mempool'] === false) {
|
||||||
|
delete client['track-mempool'];
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
@@ -369,14 +398,17 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewDonation(id: string) {
|
handleNewDonation(id: string) {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -384,43 +416,50 @@ class WebsocketHandler {
|
|||||||
client.send(JSON.stringify({ donationConfirmed: true }));
|
client.send(JSON.stringify({ donationConfirmed: true }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadingChanged(indicators: ILoadingIndicators) {
|
handleLoadingChanged(indicators: ILoadingIndicators) {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSocketDataFields({ 'loadingIndicators': indicators });
|
this.updateSocketDataFields({ 'loadingIndicators': indicators });
|
||||||
|
|
||||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.send(response);
|
client.send(response);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewConversionRates(conversionRates: ApiPrice) {
|
handleNewConversionRates(conversionRates: ApiPrice) {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSocketDataFields({ 'conversions': conversionRates });
|
this.updateSocketDataFields({ 'conversions': conversionRates });
|
||||||
|
|
||||||
const response = JSON.stringify({ conversions: conversionRates });
|
const response = JSON.stringify({ conversions: conversionRates });
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.send(response);
|
client.send(response);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewStatistic(stats: OptimizedStatistic) {
|
handleNewStatistic(stats: OptimizedStatistic) {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
@@ -429,7 +468,9 @@ class WebsocketHandler {
|
|||||||
'live-2h-chart': stats
|
'live-2h-chart': stats
|
||||||
});
|
});
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -440,11 +481,12 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
client.send(response);
|
client.send(response);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReorg(): void {
|
handleReorg(): void {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
@@ -455,7 +497,9 @@ class WebsocketHandler {
|
|||||||
'da': da?.previousTime ? da : undefined,
|
'da': da?.previousTime ? da : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -473,17 +517,29 @@ class WebsocketHandler {
|
|||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param newMempool
|
||||||
|
* @param mempoolSize
|
||||||
|
* @param newTransactions array of transactions added this mempool update.
|
||||||
|
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
|
||||||
|
* @param accelerationDelta
|
||||||
|
* @param candidates
|
||||||
|
*/
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates): Promise<void> {
|
candidates?: GbtCandidates): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
|
|
||||||
|
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||||
|
|
||||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||||
let added = newTransactions;
|
let added = newTransactions;
|
||||||
let removed = deletedTransactions;
|
let removed = deletedTransactions;
|
||||||
@@ -493,17 +549,18 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.RUST_GBT) {
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true);
|
||||||
} else {
|
} else {
|
||||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
|
const accelerations = memPool.getAccelerations();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
const rbfChanges = rbfCache.getRbfChanges();
|
const rbfChanges = rbfCache.getRbfChanges();
|
||||||
let rbfReplacements;
|
let rbfReplacements;
|
||||||
@@ -525,6 +582,33 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
const latestTransactions = memPool.getLatestTransactions();
|
const latestTransactions = memPool.getLatestTransactions();
|
||||||
|
|
||||||
|
if (memPool.isInSync()) {
|
||||||
|
this.mempoolSequence++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||||
|
for (const tx of newTransactions) {
|
||||||
|
if (rbfTransactions[tx.txid]) {
|
||||||
|
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||||
|
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
|
added: newTransactions.map(tx => tx.txid),
|
||||||
|
removed: deletedTransactions.map(tx => tx.txid),
|
||||||
|
mined: [],
|
||||||
|
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||||
|
};
|
||||||
|
const mempoolDelta: MempoolDelta = {
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
|
added: newTransactions,
|
||||||
|
removed: deletedTransactions.map(tx => tx.txid),
|
||||||
|
mined: [],
|
||||||
|
replaced: replacedTransactions,
|
||||||
|
};
|
||||||
|
|
||||||
// update init data
|
// update init data
|
||||||
const socketDataFields = {
|
const socketDataFields = {
|
||||||
'mempoolInfo': mempoolInfo,
|
'mempoolInfo': mempoolInfo,
|
||||||
@@ -552,7 +636,9 @@ class WebsocketHandler {
|
|||||||
// pre-compute new tracked outspends
|
// pre-compute new tracked outspends
|
||||||
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
|
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
|
||||||
const trackedTxs = new Set<string>();
|
const trackedTxs = new Set<string>();
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client['track-tx']) {
|
if (client['track-tx']) {
|
||||||
trackedTxs.add(client['track-tx']);
|
trackedTxs.add(client['track-tx']);
|
||||||
}
|
}
|
||||||
@@ -562,6 +648,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (trackedTxs.size > 0) {
|
if (trackedTxs.size > 0) {
|
||||||
for (const tx of newTransactions) {
|
for (const tx of newTransactions) {
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
@@ -581,7 +668,15 @@ class WebsocketHandler {
|
|||||||
const addressCache = this.makeAddressCache(newTransactions);
|
const addressCache = this.makeAddressCache(newTransactions);
|
||||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||||
|
|
||||||
this.wss.clients.forEach(async (client) => {
|
// pre-compute acceleration delta
|
||||||
|
const accelerationUpdate = {
|
||||||
|
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||||
|
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach(async (client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -737,10 +832,14 @@ class WebsocketHandler {
|
|||||||
position: {
|
position: {
|
||||||
...mempoolTx.position,
|
...mempoolTx.position,
|
||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
}
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
|
},
|
||||||
|
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||||
};
|
};
|
||||||
if (!mempoolTx.cpfpChecked) {
|
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
|
||||||
calculateCpfp(mempoolTx, newMempool);
|
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||||
}
|
}
|
||||||
if (mempoolTx.cpfpDirty) {
|
if (mempoolTx.cpfpDirty) {
|
||||||
positionData['cpfp'] = {
|
positionData['cpfp'] = {
|
||||||
@@ -750,7 +849,7 @@ class WebsocketHandler {
|
|||||||
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
|
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
|
||||||
sigops: mempoolTx.sigops,
|
sigops: mempoolTx.sigops,
|
||||||
adjustedVsize: mempoolTx.adjustedVsize,
|
adjustedVsize: mempoolTx.adjustedVsize,
|
||||||
acceleration: mempoolTx.acceleration
|
acceleration: mempoolTx.acceleration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
response['txPosition'] = JSON.stringify(positionData);
|
response['txPosition'] = JSON.stringify(positionData);
|
||||||
@@ -775,9 +874,12 @@ class WebsocketHandler {
|
|||||||
txInfo.position = {
|
txInfo.position = {
|
||||||
...mempoolTx.position,
|
...mempoolTx.position,
|
||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
};
|
};
|
||||||
if (!mempoolTx.cpfpChecked) {
|
if (!mempoolTx.cpfpChecked) {
|
||||||
calculateCpfp(mempoolTx, newMempool);
|
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||||
}
|
}
|
||||||
if (mempoolTx.cpfpDirty) {
|
if (mempoolTx.cpfpDirty) {
|
||||||
txInfo.cpfp = {
|
txInfo.cpfp = {
|
||||||
@@ -802,6 +904,7 @@ class WebsocketHandler {
|
|||||||
if (mBlockDeltas[index]) {
|
if (mBlockDeltas[index]) {
|
||||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||||
index: index,
|
index: index,
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
delta: mBlockDeltas[index],
|
delta: mBlockDeltas[index],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -817,17 +920,32 @@ class WebsocketHandler {
|
|||||||
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
|
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['track-mempool-txids']) {
|
||||||
|
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client['track-mempool']) {
|
||||||
|
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
|
||||||
|
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockTransactions = structuredClone(transactions);
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
|
|
||||||
@@ -837,31 +955,27 @@ class WebsocketHandler {
|
|||||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||||
|
|
||||||
const accelerations = Object.values(mempool.getAccelerations());
|
const accelerations = Object.values(mempool.getAccelerations());
|
||||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
|
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||||
|
|
||||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
memPool.removeFromSpendMap(transactions);
|
memPool.removeFromSpendMap(transactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
let projectedBlocks;
|
let projectedBlocks;
|
||||||
const auditMempool = _memPool;
|
const auditMempool = _memPool;
|
||||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||||
|
|
||||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
if (config.MEMPOOL.RUST_GBT) {
|
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
|
||||||
} else {
|
|
||||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||||
@@ -883,9 +997,11 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
BlocksAuditsRepository.$saveAudit({
|
BlocksAuditsRepository.$saveAudit({
|
||||||
|
version: 1,
|
||||||
time: block.timestamp,
|
time: block.timestamp,
|
||||||
height: block.height,
|
height: block.height,
|
||||||
hash: block.id,
|
hash: block.id,
|
||||||
|
unseenTxs: unseen,
|
||||||
addedTxs: added,
|
addedTxs: added,
|
||||||
prioritizedTxs: prioritized,
|
prioritizedTxs: prioritized,
|
||||||
missingTxs: censored,
|
missingTxs: censored,
|
||||||
@@ -937,7 +1053,7 @@ class WebsocketHandler {
|
|||||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
|
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
|
||||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
|
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
|
||||||
} else {
|
} else {
|
||||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true);
|
||||||
}
|
}
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
@@ -961,6 +1077,31 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
|
|
||||||
|
if (memPool.isInSync()) {
|
||||||
|
this.mempoolSequence++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||||
|
for (const txid of Object.keys(rbfTransactions)) {
|
||||||
|
for (const replaced of rbfTransactions[txid].replaced) {
|
||||||
|
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
mined: transactions.map(tx => tx.txid),
|
||||||
|
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||||
|
};
|
||||||
|
const mempoolDelta: MempoolDelta = {
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
|
added: [],
|
||||||
|
removed: [],
|
||||||
|
mined: transactions.map(tx => tx.txid),
|
||||||
|
replaced: replacedTransactions,
|
||||||
|
};
|
||||||
|
|
||||||
const responseCache = { ...this.socketData };
|
const responseCache = { ...this.socketData };
|
||||||
function getCachedResponse(key, data): string {
|
function getCachedResponse(key, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
@@ -969,7 +1110,9 @@ class WebsocketHandler {
|
|||||||
return responseCache[key];
|
return responseCache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1153,11 @@ class WebsocketHandler {
|
|||||||
position: {
|
position: {
|
||||||
...mempoolTx.position,
|
...mempoolTx.position,
|
||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
}
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
|
},
|
||||||
|
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1029,6 +1176,9 @@ class WebsocketHandler {
|
|||||||
...mempoolTx.position,
|
...mempoolTx.position,
|
||||||
},
|
},
|
||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1135,21 +1285,32 @@ class WebsocketHandler {
|
|||||||
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
|
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
|
||||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
|
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
|
||||||
index: index,
|
index: index,
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
|
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
|
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
|
||||||
index: index,
|
index: index,
|
||||||
|
sequence: this.mempoolSequence,
|
||||||
delta: mBlockDeltas[index],
|
delta: mBlockDeltas[index],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['track-mempool-txids']) {
|
||||||
|
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client['track-mempool']) {
|
||||||
|
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1319,7 @@ class WebsocketHandler {
|
|||||||
// and zips it together into a valid JSON object
|
// and zips it together into a valid JSON object
|
||||||
private serializeResponse(response): string {
|
private serializeResponse(response): string {
|
||||||
return '{'
|
return '{'
|
||||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
+ Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,13 +1392,15 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private printLogs(): void {
|
private printLogs(): void {
|
||||||
if (this.wss) {
|
if (this.webSocketServers.length) {
|
||||||
let numTxSubs = 0;
|
let numTxSubs = 0;
|
||||||
let numTxsSubs = 0;
|
let numTxsSubs = 0;
|
||||||
let numProjectedSubs = 0;
|
let numProjectedSubs = 0;
|
||||||
let numRbfSubs = 0;
|
let numRbfSubs = 0;
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
// TODO - Fix indentation after PR is merged
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
if (client['track-tx']) {
|
if (client['track-tx']) {
|
||||||
numTxSubs++;
|
numTxSubs++;
|
||||||
}
|
}
|
||||||
@@ -1251,8 +1414,12 @@ class WebsocketHandler {
|
|||||||
numRbfSubs++;
|
numRbfSubs++;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const count = this.wss?.clients?.size || 0;
|
let count = 0;
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
count += server.clients?.size || 0;
|
||||||
|
}
|
||||||
const diff = count - this.numClients;
|
const diff = count - this.numClients;
|
||||||
this.numClients = count;
|
this.numClients = count;
|
||||||
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
|
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface IConfig {
|
|||||||
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
|
||||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||||
HTTP_PORT: number;
|
HTTP_PORT: number;
|
||||||
|
UNIX_SOCKET_PATH: string;
|
||||||
SPAWN_CLUSTER_PROCS: number;
|
SPAWN_CLUSTER_PROCS: number;
|
||||||
API_URL_PREFIX: string;
|
API_URL_PREFIX: string;
|
||||||
POLL_RATE_MS: number;
|
POLL_RATE_MS: number;
|
||||||
@@ -28,7 +29,7 @@ interface IConfig {
|
|||||||
EXTERNAL_RETRY_INTERVAL: number;
|
EXTERNAL_RETRY_INTERVAL: number;
|
||||||
USER_AGENT: string;
|
USER_AGENT: string;
|
||||||
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||||
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
AUTOMATIC_POOLS_UPDATE: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
AUDIT: boolean;
|
AUDIT: boolean;
|
||||||
@@ -50,6 +51,7 @@ interface IConfig {
|
|||||||
REQUEST_TIMEOUT: number;
|
REQUEST_TIMEOUT: number;
|
||||||
FALLBACK_TIMEOUT: number;
|
FALLBACK_TIMEOUT: number;
|
||||||
FALLBACK: string[];
|
FALLBACK: string[];
|
||||||
|
MAX_BEHIND_TIP: number;
|
||||||
};
|
};
|
||||||
LIGHTNING: {
|
LIGHTNING: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
@@ -140,6 +142,8 @@ interface IConfig {
|
|||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
AUDIT: boolean;
|
AUDIT: boolean;
|
||||||
AUDIT_START_HEIGHT: number;
|
AUDIT_START_HEIGHT: number;
|
||||||
|
STATISTICS: boolean;
|
||||||
|
STATISTICS_START_TIME: number | string;
|
||||||
SERVERS: string[];
|
SERVERS: string[];
|
||||||
},
|
},
|
||||||
MEMPOOL_SERVICES: {
|
MEMPOOL_SERVICES: {
|
||||||
@@ -153,6 +157,7 @@ interface IConfig {
|
|||||||
},
|
},
|
||||||
FIAT_PRICE: {
|
FIAT_PRICE: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
|
PAID: boolean;
|
||||||
API_KEY: string;
|
API_KEY: string;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -164,6 +169,7 @@ const defaults: IConfig = {
|
|||||||
'NETWORK': 'mainnet',
|
'NETWORK': 'mainnet',
|
||||||
'BACKEND': 'none',
|
'BACKEND': 'none',
|
||||||
'HTTP_PORT': 8999,
|
'HTTP_PORT': 8999,
|
||||||
|
'UNIX_SOCKET_PATH': '',
|
||||||
'SPAWN_CLUSTER_PROCS': 0,
|
'SPAWN_CLUSTER_PROCS': 0,
|
||||||
'API_URL_PREFIX': '/api/v1/',
|
'API_URL_PREFIX': '/api/v1/',
|
||||||
'POLL_RATE_MS': 2000,
|
'POLL_RATE_MS': 2000,
|
||||||
@@ -183,7 +189,7 @@ const defaults: IConfig = {
|
|||||||
'EXTERNAL_RETRY_INTERVAL': 0,
|
'EXTERNAL_RETRY_INTERVAL': 0,
|
||||||
'USER_AGENT': 'mempool',
|
'USER_AGENT': 'mempool',
|
||||||
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
||||||
'AUTOMATIC_BLOCK_REINDEXING': false,
|
'AUTOMATIC_POOLS_UPDATE': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
'AUDIT': false,
|
'AUDIT': false,
|
||||||
@@ -205,6 +211,7 @@ const defaults: IConfig = {
|
|||||||
'REQUEST_TIMEOUT': 10000,
|
'REQUEST_TIMEOUT': 10000,
|
||||||
'FALLBACK_TIMEOUT': 5000,
|
'FALLBACK_TIMEOUT': 5000,
|
||||||
'FALLBACK': [],
|
'FALLBACK': [],
|
||||||
|
'MAX_BEHIND_TIP': 2,
|
||||||
},
|
},
|
||||||
'ELECTRUM': {
|
'ELECTRUM': {
|
||||||
'HOST': '127.0.0.1',
|
'HOST': '127.0.0.1',
|
||||||
@@ -295,6 +302,8 @@ const defaults: IConfig = {
|
|||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'AUDIT': false,
|
'AUDIT': false,
|
||||||
'AUDIT_START_HEIGHT': 774000,
|
'AUDIT_START_HEIGHT': 774000,
|
||||||
|
'STATISTICS': false,
|
||||||
|
'STATISTICS_START_TIME': 1481932800,
|
||||||
'SERVERS': [],
|
'SERVERS': [],
|
||||||
},
|
},
|
||||||
'MEMPOOL_SERVICES': {
|
'MEMPOOL_SERVICES': {
|
||||||
@@ -308,6 +317,7 @@ const defaults: IConfig = {
|
|||||||
},
|
},
|
||||||
'FIAT_PRICE': {
|
'FIAT_PRICE': {
|
||||||
'ENABLED': true,
|
'ENABLED': true,
|
||||||
|
'PAID': false,
|
||||||
'API_KEY': '',
|
'API_KEY': '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import * as fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
||||||
import { LogLevel } from './logger';
|
import logger, { LogLevel } from './logger';
|
||||||
import logger from './logger';
|
|
||||||
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,15 @@ import redisCache from './api/redis-cache';
|
|||||||
import accelerationApi from './api/services/acceleration';
|
import accelerationApi from './api/services/acceleration';
|
||||||
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
||||||
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||||
|
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||||
|
import aboutRoutes from './api/about.routes';
|
||||||
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
|
private wssUnixSocket: WebSocket.Server | undefined;
|
||||||
private server: http.Server | undefined;
|
private server: http.Server | undefined;
|
||||||
|
private serverUnixSocket: http.Server | undefined;
|
||||||
private app: Application;
|
private app: Application;
|
||||||
private currentBackendRetryInterval = 1;
|
private currentBackendRetryInterval = 1;
|
||||||
private backendRetryCount = 0;
|
private backendRetryCount = 0;
|
||||||
@@ -127,6 +132,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
.use(express.urlencoded({ extended: true }))
|
.use(express.urlencoded({ extended: true }))
|
||||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||||
|
.use(express.json())
|
||||||
;
|
;
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
||||||
@@ -135,11 +141,16 @@ class Server {
|
|||||||
|
|
||||||
this.server = http.createServer(this.app);
|
this.server = http.createServer(this.app);
|
||||||
this.wss = new WebSocket.Server({ server: this.server });
|
this.wss = new WebSocket.Server({ server: this.server });
|
||||||
|
if (config.MEMPOOL.UNIX_SOCKET_PATH) {
|
||||||
|
this.serverUnixSocket = http.createServer(this.app);
|
||||||
|
this.wssUnixSocket = new WebSocket.Server({ server: this.serverUnixSocket });
|
||||||
|
}
|
||||||
|
|
||||||
this.setUpWebsocketHandling();
|
this.setUpWebsocketHandling();
|
||||||
|
|
||||||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||||
await syncAssets.syncAssets$();
|
await syncAssets.syncAssets$();
|
||||||
|
await mempoolBlocks.updatePools$();
|
||||||
if (config.MEMPOOL.ENABLED) {
|
if (config.MEMPOOL.ENABLED) {
|
||||||
if (config.MEMPOOL.CACHE_ENABLED) {
|
if (config.MEMPOOL.CACHE_ENABLED) {
|
||||||
await diskCache.$loadMempoolCache();
|
await diskCache.$loadMempoolCache();
|
||||||
@@ -190,6 +201,16 @@ class Server {
|
|||||||
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.serverUnixSocket) {
|
||||||
|
this.serverUnixSocket.listen(config.MEMPOOL.UNIX_SOCKET_PATH, () => {
|
||||||
|
if (worker) {
|
||||||
|
logger.info(`Mempool Server worker #${process.pid} started`);
|
||||||
|
} else {
|
||||||
|
logger.notice(`Mempool Server is listening on ${config.MEMPOOL.UNIX_SOCKET_PATH}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMainUpdateLoop(): Promise<void> {
|
async runMainUpdateLoop(): Promise<void> {
|
||||||
@@ -208,7 +229,7 @@ class Server {
|
|||||||
const newMempool = await bitcoinApi.$getRawMempool();
|
const newMempool = await bitcoinApi.$getRawMempool();
|
||||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||||
const numHandledBlocks = await blocks.$updateBlocks();
|
const numHandledBlocks = await blocks.$updateBlocks();
|
||||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||||
if (numHandledBlocks === 0) {
|
if (numHandledBlocks === 0) {
|
||||||
@@ -263,8 +284,12 @@ class Server {
|
|||||||
|
|
||||||
setUpWebsocketHandling(): void {
|
setUpWebsocketHandling(): void {
|
||||||
if (this.wss) {
|
if (this.wss) {
|
||||||
websocketHandler.setWebsocketServer(this.wss);
|
websocketHandler.addWebsocketServer(this.wss);
|
||||||
}
|
}
|
||||||
|
if (this.wssUnixSocket) {
|
||||||
|
websocketHandler.addWebsocketServer(this.wssUnixSocket);
|
||||||
|
}
|
||||||
|
|
||||||
if (Common.isLiquid() && config.DATABASE.ENABLED) {
|
if (Common.isLiquid() && config.DATABASE.ENABLED) {
|
||||||
blocks.setNewBlockCallback(async () => {
|
blocks.setNewBlockCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -305,6 +330,12 @@ class Server {
|
|||||||
nodesRoutes.initRoutes(this.app);
|
nodesRoutes.initRoutes(this.app);
|
||||||
channelsRoutes.initRoutes(this.app);
|
channelsRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||||
|
accelerationRoutes.initRoutes(this.app);
|
||||||
|
}
|
||||||
|
if (!config.MEMPOOL.OFFICIAL) {
|
||||||
|
aboutRoutes.initRoutes(this.app);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
healthCheck(): void {
|
healthCheck(): void {
|
||||||
@@ -332,6 +363,12 @@ class Server {
|
|||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
DB.releasePidLock();
|
DB.releasePidLock();
|
||||||
}
|
}
|
||||||
|
this.server?.close();
|
||||||
|
this.serverUnixSocket?.close();
|
||||||
|
this.wss?.close();
|
||||||
|
if (this.wssUnixSocket) {
|
||||||
|
this.wssUnixSocket.close();
|
||||||
|
}
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import priceUpdater from './tasks/price-updater';
|
|||||||
import PricesRepository from './repositories/PricesRepository';
|
import PricesRepository from './repositories/PricesRepository';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import auditReplicator from './replication/AuditReplication';
|
import auditReplicator from './replication/AuditReplication';
|
||||||
|
import statisticsReplicator from './replication/StatisticsReplication';
|
||||||
import AccelerationRepository from './repositories/AccelerationRepository';
|
import AccelerationRepository from './repositories/AccelerationRepository';
|
||||||
|
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
export interface CoreIndex {
|
export interface CoreIndex {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -181,6 +183,7 @@ class Indexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.runSingleTask('blocksPrices');
|
this.runSingleTask('blocksPrices');
|
||||||
|
await blocks.$indexCoinbaseAddresses();
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
@@ -188,7 +191,9 @@ class Indexer {
|
|||||||
await blocks.$generateCPFPDatabase();
|
await blocks.$generateCPFPDatabase();
|
||||||
await blocks.$generateAuditStats();
|
await blocks.$generateAuditStats();
|
||||||
await auditReplicator.$sync();
|
await auditReplicator.$sync();
|
||||||
|
await statisticsReplicator.$sync();
|
||||||
await AccelerationRepository.$indexPastAccelerations();
|
await AccelerationRepository.$indexPastAccelerations();
|
||||||
|
await BlocksAuditsRepository.$migrateAuditsV0toV1();
|
||||||
// do not wait for classify blocks to finish
|
// do not wait for classify blocks to finish
|
||||||
blocks.$classifyBlocks();
|
blocks.$classifyBlocks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockAudit {
|
export interface BlockAudit {
|
||||||
|
version: number,
|
||||||
time: number,
|
time: number,
|
||||||
height: number,
|
height: number,
|
||||||
hash: string,
|
hash: string,
|
||||||
|
unseenTxs: string[],
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
freshTxs: string[],
|
freshTxs: string[],
|
||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
@@ -42,6 +44,19 @@ export interface BlockAudit {
|
|||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees?: number,
|
expectedFees?: number,
|
||||||
expectedWeight?: number,
|
expectedWeight?: number,
|
||||||
|
template?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionAudit {
|
||||||
|
seen?: boolean;
|
||||||
|
expected?: boolean;
|
||||||
|
added?: boolean;
|
||||||
|
prioritized?: boolean;
|
||||||
|
delayed?: number;
|
||||||
|
accelerated?: boolean;
|
||||||
|
conflict?: boolean;
|
||||||
|
coinbase?: boolean;
|
||||||
|
firstSeen?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditScore {
|
export interface AuditScore {
|
||||||
@@ -71,6 +86,22 @@ export interface MempoolBlockDelta {
|
|||||||
changed: MempoolDeltaChange[];
|
changed: MempoolDeltaChange[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MempoolDeltaTxids {
|
||||||
|
sequence: number,
|
||||||
|
added: string[];
|
||||||
|
removed: string[];
|
||||||
|
mined: string[];
|
||||||
|
replaced: { replaced: string, by: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MempoolDelta {
|
||||||
|
sequence: number,
|
||||||
|
added: MempoolTransactionExtended[];
|
||||||
|
removed: string[];
|
||||||
|
mined: string[];
|
||||||
|
replaced: { replaced: string, by: TransactionExtended }[];
|
||||||
|
}
|
||||||
|
|
||||||
interface VinStrippedToScriptsig {
|
interface VinStrippedToScriptsig {
|
||||||
scriptsig: string;
|
scriptsig: string;
|
||||||
}
|
}
|
||||||
@@ -95,6 +126,9 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
vsize: number,
|
vsize: number,
|
||||||
};
|
};
|
||||||
acceleration?: boolean;
|
acceleration?: boolean;
|
||||||
|
acceleratedBy?: number[];
|
||||||
|
acceleratedAt?: number;
|
||||||
|
feeDelta?: number;
|
||||||
replacement?: boolean;
|
replacement?: boolean;
|
||||||
uid?: number;
|
uid?: number;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
@@ -192,6 +226,7 @@ export interface CpfpInfo {
|
|||||||
sigops?: number;
|
sigops?: number;
|
||||||
adjustedVsize?: number,
|
adjustedVsize?: number,
|
||||||
acceleration?: boolean,
|
acceleration?: boolean,
|
||||||
|
fee?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionStripped {
|
export interface TransactionStripped {
|
||||||
@@ -270,6 +305,7 @@ export interface BlockExtension {
|
|||||||
coinbaseRaw: string;
|
coinbaseRaw: string;
|
||||||
orphans: OrphanedBlock[] | null;
|
orphans: OrphanedBlock[] | null;
|
||||||
coinbaseAddress: string | null;
|
coinbaseAddress: string | null;
|
||||||
|
coinbaseAddresses: string[] | null;
|
||||||
coinbaseSignature: string | null;
|
coinbaseSignature: string | null;
|
||||||
coinbaseSignatureAscii: string | null;
|
coinbaseSignatureAscii: string | null;
|
||||||
virtualSize: number;
|
virtualSize: number;
|
||||||
@@ -349,8 +385,9 @@ export interface CpfpCluster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CpfpSummary {
|
export interface CpfpSummary {
|
||||||
transactions: TransactionExtended[];
|
transactions: MempoolTransactionExtended[];
|
||||||
clusters: CpfpCluster[];
|
clusters: CpfpCluster[];
|
||||||
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Statistic {
|
export interface Statistic {
|
||||||
@@ -406,6 +443,7 @@ export interface Statistic {
|
|||||||
|
|
||||||
export interface OptimizedStatistic {
|
export interface OptimizedStatistic {
|
||||||
added: string;
|
added: string;
|
||||||
|
count: number;
|
||||||
vbytes_per_second: number;
|
vbytes_per_second: number;
|
||||||
total_fee: number;
|
total_fee: number;
|
||||||
mempool_byte_weight: number;
|
mempool_byte_weight: number;
|
||||||
@@ -415,7 +453,7 @@ export interface OptimizedStatistic {
|
|||||||
|
|
||||||
export interface TxTrackingInfo {
|
export interface TxTrackingInfo {
|
||||||
replacedBy?: string,
|
replacedBy?: string,
|
||||||
position?: { block: number, vsize: number, accelerated?: boolean },
|
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
|
||||||
cpfp?: {
|
cpfp?: {
|
||||||
ancestors?: Ancestor[],
|
ancestors?: Ancestor[],
|
||||||
bestDescendant?: Ancestor | null,
|
bestDescendant?: Ancestor | null,
|
||||||
@@ -426,6 +464,9 @@ export interface TxTrackingInfo {
|
|||||||
},
|
},
|
||||||
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
|
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
|
||||||
accelerated?: boolean,
|
accelerated?: boolean,
|
||||||
|
acceleratedBy?: number[],
|
||||||
|
acceleratedAt?: number,
|
||||||
|
feeDelta?: number,
|
||||||
confirmed?: boolean
|
confirmed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ class AuditReplication {
|
|||||||
const missingAudits = await this.$getMissingAuditBlocks();
|
const missingAudits = await this.$getMissingAuditBlocks();
|
||||||
|
|
||||||
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||||
|
|
||||||
let totalSynced = 0;
|
let totalSynced = 0;
|
||||||
let totalMissed = 0;
|
let totalMissed = 0;
|
||||||
let loggerTimer = Date.now();
|
let loggerTimer = Date.now();
|
||||||
// process missing audits in batches of
|
// process missing audits in batches of BATCH_SIZE
|
||||||
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
||||||
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
||||||
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
||||||
@@ -109,9 +109,11 @@ class AuditReplication {
|
|||||||
version: 1,
|
version: 1,
|
||||||
});
|
});
|
||||||
await blocksAuditsRepository.$saveAudit({
|
await blocksAuditsRepository.$saveAudit({
|
||||||
|
version: auditSummary.version || 0,
|
||||||
hash: blockHash,
|
hash: blockHash,
|
||||||
height: auditSummary.height,
|
height: auditSummary.height,
|
||||||
time: auditSummary.timestamp || auditSummary.time,
|
time: auditSummary.timestamp || auditSummary.time,
|
||||||
|
unseenTxs: auditSummary.unseenTxs || [],
|
||||||
missingTxs: auditSummary.missingTxs || [],
|
missingTxs: auditSummary.missingTxs || [],
|
||||||
addedTxs: auditSummary.addedTxs || [],
|
addedTxs: auditSummary.addedTxs || [],
|
||||||
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||||
|
|||||||
237
backend/src/replication/StatisticsReplication.ts
Normal file
237
backend/src/replication/StatisticsReplication.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { $sync } from './replicator';
|
||||||
|
import config from '../config';
|
||||||
|
import { Common } from '../api/common';
|
||||||
|
import statistics from '../api/statistics/statistics-api';
|
||||||
|
|
||||||
|
interface MissingStatistics {
|
||||||
|
'24h': Set<number>;
|
||||||
|
'1w': Set<number>;
|
||||||
|
'1m': Set<number>;
|
||||||
|
'3m': Set<number>;
|
||||||
|
'6m': Set<number>;
|
||||||
|
'2y': Set<number>;
|
||||||
|
'all': Set<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = {
|
||||||
|
'24h': 60,
|
||||||
|
'1w': 300,
|
||||||
|
'1m': 1800,
|
||||||
|
'3m': 7200,
|
||||||
|
'6m': 10800,
|
||||||
|
'2y': 28800,
|
||||||
|
'all': 43200,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs missing statistics data from trusted servers
|
||||||
|
*/
|
||||||
|
class StatisticsReplication {
|
||||||
|
inProgress: boolean = false;
|
||||||
|
|
||||||
|
public async $sync(): Promise<void> {
|
||||||
|
if (!config.REPLICATION.ENABLED || !config.REPLICATION.STATISTICS || !config.STATISTICS.ENABLED) {
|
||||||
|
// replication not enabled, or statistics not enabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.inProgress) {
|
||||||
|
logger.info(`StatisticsReplication sync already in progress`, 'Replication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
const missingStatistics = await this.$getMissingStatistics();
|
||||||
|
const missingIntervals = Object.keys(missingStatistics).filter(key => missingStatistics[key].size > 0);
|
||||||
|
const totalMissing = missingIntervals.reduce((total, key) => total + missingStatistics[key].size, 0);
|
||||||
|
|
||||||
|
if (totalMissing === 0) {
|
||||||
|
this.inProgress = false;
|
||||||
|
logger.info(`Statistics table is complete, no replication needed`, 'Replication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const interval of missingIntervals) {
|
||||||
|
logger.debug(`Missing ${missingStatistics[interval].size} statistics rows in '${interval}' timespan`, 'Replication');
|
||||||
|
}
|
||||||
|
logger.debug(`Fetching ${missingIntervals.join(', ')} statistics endpoints from trusted servers to fill ${totalMissing} rows missing in statistics`, 'Replication');
|
||||||
|
|
||||||
|
let totalSynced = 0;
|
||||||
|
let totalMissed = 0;
|
||||||
|
|
||||||
|
for (const interval of missingIntervals) {
|
||||||
|
const results = await this.$syncStatistics(interval, missingStatistics[interval]);
|
||||||
|
totalSynced += results.synced;
|
||||||
|
totalMissed += results.missed;
|
||||||
|
|
||||||
|
logger.info(`Found ${totalSynced} / ${totalSynced + totalMissed} of ${totalMissing} missing statistics rows`, 'Replication');
|
||||||
|
await Common.sleep$(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Synced ${totalSynced} statistics rows, ${totalMissed} still missing`, 'Replication');
|
||||||
|
|
||||||
|
this.inProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $syncStatistics(interval: string, missingTimes: Set<number>): Promise<any> {
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
let synced = 0;
|
||||||
|
let missed = new Set(missingTimes);
|
||||||
|
const syncResult = await $sync(`/api/v1/statistics/${interval}`);
|
||||||
|
if (syncResult && syncResult.data?.length) {
|
||||||
|
success = true;
|
||||||
|
logger.info(`Fetched /api/v1/statistics/${interval} from ${syncResult.server}`);
|
||||||
|
|
||||||
|
for (const stat of syncResult.data) {
|
||||||
|
const time = this.roundToNearestStep(stat.added, steps[interval]);
|
||||||
|
if (missingTimes.has(time)) {
|
||||||
|
try {
|
||||||
|
await statistics.$create(statistics.mapOptimizedStatisticToStatistic([stat])[0], true);
|
||||||
|
if (missed.delete(time)) {
|
||||||
|
synced++;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Failed to insert statistics row at ${stat.added} (${interval}) from ${syncResult.server}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.warn(`An error occured when trying to fetch /api/v1/statistics/${interval}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success, synced, missed: missed.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getMissingStatistics(): Promise<MissingStatistics> {
|
||||||
|
try {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const day = 60 * 60 * 24;
|
||||||
|
|
||||||
|
const startTime = this.getStartTimeFromConfig();
|
||||||
|
|
||||||
|
const missingStatistics: MissingStatistics = {
|
||||||
|
'24h': new Set<number>(),
|
||||||
|
'1w': new Set<number>(),
|
||||||
|
'1m': new Set<number>(),
|
||||||
|
'3m': new Set<number>(),
|
||||||
|
'6m': new Set<number>(),
|
||||||
|
'2y': new Set<number>(),
|
||||||
|
'all': new Set<number>()
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervals = [ // [start, end, label ]
|
||||||
|
[now - day + 600, now - 60, '24h'] , // from 24 hours ago to now = 1 minute granularity
|
||||||
|
startTime < now - day ? [now - day * 7, now - day, '1w' ] : null, // from 1 week ago to 24 hours ago = 5 minutes granularity
|
||||||
|
startTime < now - day * 7 ? [now - day * 30, now - day * 7, '1m' ] : null, // from 1 month ago to 1 week ago = 30 minutes granularity
|
||||||
|
startTime < now - day * 30 ? [now - day * 90, now - day * 30, '3m' ] : null, // from 3 months ago to 1 month ago = 2 hours granularity
|
||||||
|
startTime < now - day * 90 ? [now - day * 180, now - day * 90, '6m' ] : null, // from 6 months ago to 3 months ago = 3 hours granularity
|
||||||
|
startTime < now - day * 180 ? [now - day * 365 * 2, now - day * 180, '2y' ] : null, // from 2 years ago to 6 months ago = 8 hours granularity
|
||||||
|
startTime < now - day * 365 * 2 ? [startTime, now - day * 365 * 2, 'all'] : null, // from start of statistics to 2 years ago = 12 hours granularity
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const interval of intervals) {
|
||||||
|
if (!interval) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
missingStatistics[interval[2] as string] = await this.$getMissingStatisticsInterval(interval, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return missingStatistics;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getMissingStatisticsInterval(interval: any, startTime: number): Promise<Set<number>> {
|
||||||
|
try {
|
||||||
|
const start = interval[0];
|
||||||
|
const end = interval[1];
|
||||||
|
const step = steps[interval[2]];
|
||||||
|
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT UNIX_TIMESTAMP(added) as added
|
||||||
|
FROM statistics
|
||||||
|
WHERE added >= FROM_UNIXTIME(?) AND added <= FROM_UNIXTIME(?)
|
||||||
|
GROUP BY UNIX_TIMESTAMP(added) DIV ${step} ORDER BY statistics.added DESC
|
||||||
|
`, [start, end]);
|
||||||
|
|
||||||
|
const startingTime = Math.max(startTime, start) - Math.max(startTime, start) % step;
|
||||||
|
|
||||||
|
const timeSteps: number[] = [];
|
||||||
|
for (let time = startingTime; time < end; time += step) {
|
||||||
|
timeSteps.push(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSteps.length === 0) {
|
||||||
|
return new Set<number>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedTimesAlreadyHere: number[] = Array.from(new Set(rows.map(row => this.roundToNearestStep(row.added, step))));
|
||||||
|
|
||||||
|
const missingTimes = timeSteps.filter(time => !roundedTimesAlreadyHere.includes(time)).filter((time, i, arr) => {
|
||||||
|
// Remove outsiders
|
||||||
|
if (i === 0) {
|
||||||
|
return arr[i + 1] === time + step
|
||||||
|
} else if (i === arr.length - 1) {
|
||||||
|
return arr[i - 1] === time - step;
|
||||||
|
}
|
||||||
|
return (arr[i + 1] === time + step) && (arr[i - 1] === time - step)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't bother fetching if very few rows are missing
|
||||||
|
if (missingTimes.length < timeSteps.length * 0.01) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(missingTimes);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private roundToNearestStep(time: number, step: number): number {
|
||||||
|
const remainder = time % step;
|
||||||
|
if (remainder < step / 2) {
|
||||||
|
return time - remainder;
|
||||||
|
} else {
|
||||||
|
return time + (step - remainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStartTimeFromConfig(): number {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const day = 60 * 60 * 24;
|
||||||
|
|
||||||
|
let startTime: number;
|
||||||
|
if (typeof(config.REPLICATION.STATISTICS_START_TIME) === 'string' && ['24h', '1w', '1m', '3m', '6m', '2y', 'all'].includes(config.REPLICATION.STATISTICS_START_TIME)) {
|
||||||
|
if (config.REPLICATION.STATISTICS_START_TIME === 'all') {
|
||||||
|
startTime = 1481932800;
|
||||||
|
} else if (config.REPLICATION.STATISTICS_START_TIME === '2y') {
|
||||||
|
startTime = now - day * 365 * 2;
|
||||||
|
} else if (config.REPLICATION.STATISTICS_START_TIME === '6m') {
|
||||||
|
startTime = now - day * 180;
|
||||||
|
} else if (config.REPLICATION.STATISTICS_START_TIME === '3m') {
|
||||||
|
startTime = now - day * 90;
|
||||||
|
} else if (config.REPLICATION.STATISTICS_START_TIME === '1m') {
|
||||||
|
startTime = now - day * 30;
|
||||||
|
} else if (config.REPLICATION.STATISTICS_START_TIME === '1w') {
|
||||||
|
startTime = now - day * 7;
|
||||||
|
} else {
|
||||||
|
startTime = now - day;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startTime = Math.max(config.REPLICATION.STATISTICS_START_TIME as number || 1481932800, 1481932800);
|
||||||
|
}
|
||||||
|
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StatisticsReplication();
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
|
import { AccelerationInfo } from '../api/acceleration/acceleration';
|
||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
@@ -6,15 +6,17 @@ import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
|
|||||||
import { Common } from '../api/common';
|
import { Common } from '../api/common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import accelerationApi, { Acceleration } from '../api/services/acceleration';
|
import accelerationApi, { Acceleration, AccelerationHistory } from '../api/services/acceleration';
|
||||||
import accelerationCosts from '../api/acceleration';
|
import accelerationCosts from '../api/acceleration/acceleration';
|
||||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||||
import transactionUtils from '../api/transaction-utils';
|
import transactionUtils from '../api/transaction-utils';
|
||||||
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||||
|
import { makeBlockTemplate } from '../api/mini-miner';
|
||||||
|
|
||||||
export interface PublicAcceleration {
|
export interface PublicAcceleration {
|
||||||
txid: string,
|
txid: string,
|
||||||
height: number,
|
height: number,
|
||||||
|
added: number,
|
||||||
pool: {
|
pool: {
|
||||||
id: number,
|
id: number,
|
||||||
slug: string,
|
slug: string,
|
||||||
@@ -29,15 +31,20 @@ export interface PublicAcceleration {
|
|||||||
class AccelerationRepository {
|
class AccelerationRepository {
|
||||||
private bidBoostV2Activated = 831580;
|
private bidBoostV2Activated = 831580;
|
||||||
|
|
||||||
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> {
|
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise<void> {
|
||||||
|
const accelerationMap: { [txid: string]: Acceleration } = {};
|
||||||
|
for (const acc of accelerationData) {
|
||||||
|
accelerationMap[acc.txid] = acc;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await DB.query(`
|
await DB.query(`
|
||||||
INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
|
INSERT INTO accelerations(txid, requested, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
|
||||||
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
|
VALUE (?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
height = ?
|
height = ?
|
||||||
`, [
|
`, [
|
||||||
acceleration.txSummary.txid,
|
acceleration.txSummary.txid,
|
||||||
|
accelerationMap[acceleration.txSummary.txid].added,
|
||||||
block.timestamp,
|
block.timestamp,
|
||||||
block.height,
|
block.height,
|
||||||
pool_id,
|
pool_id,
|
||||||
@@ -64,7 +71,7 @@ class AccelerationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let query = `
|
let query = `
|
||||||
SELECT * FROM accelerations
|
SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations
|
||||||
JOIN pools on pools.unique_id = accelerations.pool
|
JOIN pools on pools.unique_id = accelerations.pool
|
||||||
`;
|
`;
|
||||||
let params: any[] = [];
|
let params: any[] = [];
|
||||||
@@ -99,6 +106,7 @@ class AccelerationRepository {
|
|||||||
return rows.map(row => ({
|
return rows.map(row => ({
|
||||||
txid: row.txid,
|
txid: row.txid,
|
||||||
height: row.height,
|
height: row.height,
|
||||||
|
added: row.requested_timestamp || row.block_timestamp,
|
||||||
pool: {
|
pool: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
slug: row.slug,
|
slug: row.slug,
|
||||||
@@ -184,6 +192,7 @@ class AccelerationRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modifies block transactions
|
||||||
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||||
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
@@ -202,9 +211,18 @@ class AccelerationRepository {
|
|||||||
const tx = blockTxs[acc.txid];
|
const tx = blockTxs[acc.txid];
|
||||||
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
||||||
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
||||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
|
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let anyConfirmed = false;
|
||||||
|
for (const acc of accelerations) {
|
||||||
|
if (blockTxs[acc.txid]) {
|
||||||
|
anyConfirmed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anyConfirmed) {
|
||||||
|
accelerationApi.accelerationConfirmed();
|
||||||
|
}
|
||||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||||
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
|
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
|
||||||
if (block.height === lastSyncedHeight + 1) {
|
if (block.height === lastSyncedHeight + 1) {
|
||||||
@@ -230,13 +248,15 @@ class AccelerationRepository {
|
|||||||
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
|
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
|
||||||
|
|
||||||
// Fetch accelerations from mempool.space since the last synced block;
|
// Fetch accelerations from mempool.space since the last synced block;
|
||||||
const accelerationsByBlock = {};
|
const accelerationsByBlock: {[height: number]: AccelerationHistory[]} = {};
|
||||||
const blockHashes = {};
|
const blockHashes = {};
|
||||||
let done = false;
|
let done = false;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
try {
|
try {
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
// don't DDoS the services backend
|
||||||
|
Common.sleep$(500 + (Math.random() * 1000));
|
||||||
const accelerations = await accelerationApi.$fetchAccelerationHistory(page);
|
const accelerations = await accelerationApi.$fetchAccelerationHistory(page);
|
||||||
page++;
|
page++;
|
||||||
if (!accelerations?.length) {
|
if (!accelerations?.length) {
|
||||||
@@ -297,12 +317,16 @@ class AccelerationRepository {
|
|||||||
const feeStats = Common.calcEffectiveFeeStatistics(template);
|
const feeStats = Common.calcEffectiveFeeStatistics(template);
|
||||||
boostRate = feeStats.medianFee;
|
boostRate = feeStats.medianFee;
|
||||||
}
|
}
|
||||||
|
const accelerationSummaries = accelerations.map(acc => ({
|
||||||
|
...acc,
|
||||||
|
pools: acc.pools,
|
||||||
|
}))
|
||||||
for (const acc of accelerations) {
|
for (const acc of accelerations) {
|
||||||
if (blockTxs[acc.txid]) {
|
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
|
||||||
const tx = blockTxs[acc.txid];
|
const tx = blockTxs[acc.txid];
|
||||||
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
||||||
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
||||||
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
|
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, accelerationSummaries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.$setLastSyncedHeight(height);
|
await this.$setLastSyncedHeight(height);
|
||||||
@@ -317,6 +341,26 @@ class AccelerationRepository {
|
|||||||
|
|
||||||
logger.debug(`Indexing accelerations completed`);
|
logger.debug(`Indexing accelerations completed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete accelerations from the database above blockHeight
|
||||||
|
*/
|
||||||
|
public async $deleteAccelerationsFrom(blockHeight: number): Promise<void> {
|
||||||
|
logger.info(`Delete newer accelerations from height ${blockHeight} from the database`);
|
||||||
|
try {
|
||||||
|
const currentSyncedHeight = await this.$getLastSyncedHeight();
|
||||||
|
if (currentSyncedHeight >= blockHeight) {
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE state
|
||||||
|
SET number = ?
|
||||||
|
WHERE name = 'last_acceleration_block'
|
||||||
|
`, [blockHeight - 1]);
|
||||||
|
}
|
||||||
|
await DB.query(`DELETE FROM accelerations where height >= ${blockHeight}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot delete indexed accelerations. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AccelerationRepository();
|
export default new AccelerationRepository();
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import blocks from '../api/blocks';
|
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||||
|
import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
interface MigrationAudit {
|
||||||
|
version: number,
|
||||||
|
height: number,
|
||||||
|
id: string,
|
||||||
|
timestamp: number,
|
||||||
|
prioritizedTxs: string[],
|
||||||
|
acceleratedTxs: string[],
|
||||||
|
template: TransactionStripped[],
|
||||||
|
transactions: TransactionStripped[],
|
||||||
|
}
|
||||||
|
|
||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
@@ -62,24 +73,30 @@ class BlocksAuditRepositories {
|
|||||||
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
`SELECT
|
||||||
template,
|
blocks_audits.version,
|
||||||
missing_txs as missingTxs,
|
blocks_audits.height,
|
||||||
added_txs as addedTxs,
|
blocks_audits.hash as id,
|
||||||
prioritized_txs as prioritizedTxs,
|
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||||
fresh_txs as freshTxs,
|
template,
|
||||||
sigop_txs as sigopTxs,
|
unseen_txs as unseenTxs,
|
||||||
fullrbf_txs as fullrbfTxs,
|
missing_txs as missingTxs,
|
||||||
accelerated_txs as acceleratedTxs,
|
added_txs as addedTxs,
|
||||||
match_rate as matchRate,
|
prioritized_txs as prioritizedTxs,
|
||||||
expected_fees as expectedFees,
|
fresh_txs as freshTxs,
|
||||||
expected_weight as expectedWeight
|
sigop_txs as sigopTxs,
|
||||||
|
fullrbf_txs as fullrbfTxs,
|
||||||
|
accelerated_txs as acceleratedTxs,
|
||||||
|
match_rate as matchRate,
|
||||||
|
expected_fees as expectedFees,
|
||||||
|
expected_weight as expectedWeight
|
||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||||
WHERE blocks_audits.hash = ?
|
WHERE blocks_audits.hash = ?
|
||||||
`, [hash]);
|
`, [hash]);
|
||||||
|
|
||||||
if (rows.length) {
|
if (rows.length) {
|
||||||
|
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||||
@@ -98,6 +115,42 @@ class BlocksAuditRepositories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||||
|
try {
|
||||||
|
const blockAudit = await this.$getBlockAudit(hash);
|
||||||
|
|
||||||
|
if (blockAudit) {
|
||||||
|
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||||
|
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||||
|
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
|
||||||
|
const isConflict = blockAudit.fullrbfTxs.includes(txid);
|
||||||
|
let isExpected = false;
|
||||||
|
let firstSeen = undefined;
|
||||||
|
blockAudit.template?.forEach(tx => {
|
||||||
|
if (tx.txid === txid) {
|
||||||
|
isExpected = true;
|
||||||
|
firstSeen = tx.time;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||||
|
|
||||||
|
return {
|
||||||
|
seen: wasSeen,
|
||||||
|
expected: isExpected,
|
||||||
|
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||||
|
prioritized: isPrioritized,
|
||||||
|
conflict: isConflict,
|
||||||
|
accelerated: isAccelerated,
|
||||||
|
firstSeen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
@@ -151,6 +204,96 @@ class BlocksAuditRepositories {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Migrate audits from v0 to v1
|
||||||
|
*/
|
||||||
|
public async $migrateAuditsV0toV1(): Promise<void> {
|
||||||
|
try {
|
||||||
|
let done = false;
|
||||||
|
let processed = 0;
|
||||||
|
let lastHeight;
|
||||||
|
while (!done) {
|
||||||
|
const [toMigrate]: MigrationAudit[][] = await DB.query(
|
||||||
|
`SELECT
|
||||||
|
blocks_audits.height as height,
|
||||||
|
blocks_audits.hash as id,
|
||||||
|
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||||
|
blocks_summaries.transactions as transactions,
|
||||||
|
blocks_templates.template as template,
|
||||||
|
blocks_audits.prioritized_txs as prioritizedTxs,
|
||||||
|
blocks_audits.accelerated_txs as acceleratedTxs
|
||||||
|
FROM blocks_audits
|
||||||
|
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||||
|
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||||
|
WHERE blocks_audits.version = 0
|
||||||
|
AND blocks_summaries.version = 2
|
||||||
|
ORDER BY blocks_audits.height DESC
|
||||||
|
LIMIT 100
|
||||||
|
`) as any[];
|
||||||
|
|
||||||
|
if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastHeight = toMigrate[0].height;
|
||||||
|
|
||||||
|
logger.info(`migrating ${toMigrate.length} audits to version 1`);
|
||||||
|
|
||||||
|
for (const audit of toMigrate) {
|
||||||
|
// unpack JSON-serialized transaction lists
|
||||||
|
audit.transactions = JSON.parse((audit.transactions as any as string) || '[]');
|
||||||
|
audit.template = JSON.parse((audit.template as any as string) || '[]');
|
||||||
|
|
||||||
|
// we know transactions in the template, or marked "prioritized" or "accelerated"
|
||||||
|
// were seen in our mempool before the block was mined.
|
||||||
|
const isSeen = new Set<string>();
|
||||||
|
for (const tx of audit.template) {
|
||||||
|
isSeen.add(tx.txid);
|
||||||
|
}
|
||||||
|
for (const txid of audit.prioritizedTxs) {
|
||||||
|
isSeen.add(txid);
|
||||||
|
}
|
||||||
|
for (const txid of audit.acceleratedTxs) {
|
||||||
|
isSeen.add(txid);
|
||||||
|
}
|
||||||
|
const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid));
|
||||||
|
|
||||||
|
// identify "prioritized" transactions
|
||||||
|
const prioritizedTxs: string[] = [];
|
||||||
|
let lastEffectiveRate = 0;
|
||||||
|
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||||
|
// Transactions should appear in ascending order of mining priority.
|
||||||
|
for (let i = audit.transactions.length - 1; i > 0; i--) {
|
||||||
|
const blockTx = audit.transactions[i];
|
||||||
|
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||||
|
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||||
|
// so exclude from the analysis.
|
||||||
|
if ((blockTx.rate || 0) < lastEffectiveRate) {
|
||||||
|
prioritizedTxs.push(blockTx.txid);
|
||||||
|
} else {
|
||||||
|
lastEffectiveRate = blockTx.rate || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update audit in the database
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE blocks_audits SET
|
||||||
|
version = ?,
|
||||||
|
unseen_txs = ?,
|
||||||
|
prioritized_txs = ?
|
||||||
|
WHERE hash = ?
|
||||||
|
`, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
processed += toMigrate.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`migrated ${processed} audits to version 1`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksAuditRepositories();
|
export default new BlocksAuditRepositories();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logger from '../logger';
|
|||||||
import { Common } from '../api/common';
|
import { Common } from '../api/common';
|
||||||
import PoolsRepository from './PoolsRepository';
|
import PoolsRepository from './PoolsRepository';
|
||||||
import HashratesRepository from './HashratesRepository';
|
import HashratesRepository from './HashratesRepository';
|
||||||
import { RowDataPacket, escape } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
||||||
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
||||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
||||||
@@ -40,6 +40,7 @@ interface DatabaseBlock {
|
|||||||
avgFeeRate: number;
|
avgFeeRate: number;
|
||||||
coinbaseRaw: string;
|
coinbaseRaw: string;
|
||||||
coinbaseAddress: string;
|
coinbaseAddress: string;
|
||||||
|
coinbaseAddresses: string;
|
||||||
coinbaseSignature: string;
|
coinbaseSignature: string;
|
||||||
coinbaseSignatureAscii: string;
|
coinbaseSignatureAscii: string;
|
||||||
avgTxSize: number;
|
avgTxSize: number;
|
||||||
@@ -82,6 +83,7 @@ const BLOCK_DB_FIELDS = `
|
|||||||
blocks.avg_fee_rate AS avgFeeRate,
|
blocks.avg_fee_rate AS avgFeeRate,
|
||||||
blocks.coinbase_raw AS coinbaseRaw,
|
blocks.coinbase_raw AS coinbaseRaw,
|
||||||
blocks.coinbase_address AS coinbaseAddress,
|
blocks.coinbase_address AS coinbaseAddress,
|
||||||
|
blocks.coinbase_addresses AS coinbaseAddresses,
|
||||||
blocks.coinbase_signature AS coinbaseSignature,
|
blocks.coinbase_signature AS coinbaseSignature,
|
||||||
blocks.coinbase_signature_ascii AS coinbaseSignatureAscii,
|
blocks.coinbase_signature_ascii AS coinbaseSignatureAscii,
|
||||||
blocks.avg_tx_size AS avgTxSize,
|
blocks.avg_tx_size AS avgTxSize,
|
||||||
@@ -114,7 +116,7 @@ class BlocksRepository {
|
|||||||
pool_id, fees, fee_span, median_fee,
|
pool_id, fees, fee_span, median_fee,
|
||||||
reward, version, bits, nonce,
|
reward, version, bits, nonce,
|
||||||
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
|
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
|
||||||
median_timestamp, header, coinbase_address,
|
median_timestamp, header, coinbase_address, coinbase_addresses,
|
||||||
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
|
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
|
||||||
total_inputs, total_outputs, total_input_amt, total_output_amt,
|
total_inputs, total_outputs, total_input_amt, total_output_amt,
|
||||||
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
|
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
|
||||||
@@ -125,7 +127,7 @@ class BlocksRepository {
|
|||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
FROM_UNIXTIME(?), ?, ?,
|
FROM_UNIXTIME(?), ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
@@ -161,6 +163,7 @@ class BlocksRepository {
|
|||||||
block.mediantime,
|
block.mediantime,
|
||||||
block.extras.header,
|
block.extras.header,
|
||||||
block.extras.coinbaseAddress,
|
block.extras.coinbaseAddress,
|
||||||
|
block.extras.coinbaseAddresses ? JSON.stringify(block.extras.coinbaseAddresses) : null,
|
||||||
truncatedCoinbaseSignature,
|
truncatedCoinbaseSignature,
|
||||||
block.extras.utxoSetSize,
|
block.extras.utxoSetSize,
|
||||||
block.extras.utxoSetChange,
|
block.extras.utxoSetChange,
|
||||||
@@ -529,7 +532,7 @@ class BlocksRepository {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
|
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
@@ -663,7 +666,7 @@ class BlocksRepository {
|
|||||||
/**
|
/**
|
||||||
* Get the historical averaged block fees
|
* Get the historical averaged block fees
|
||||||
*/
|
*/
|
||||||
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
|
public async $getHistoricalBlockFees(div: number, interval: string | null, timespan?: {from: number, to: number}): Promise<any> {
|
||||||
try {
|
try {
|
||||||
let query = `SELECT
|
let query = `SELECT
|
||||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||||
@@ -677,6 +680,8 @@ class BlocksRepository {
|
|||||||
|
|
||||||
if (interval !== null) {
|
if (interval !== null) {
|
||||||
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||||
|
} else if (timespan) {
|
||||||
|
query += ` WHERE blockTimestamp BETWEEN FROM_UNIXTIME(${timespan.from}) AND FROM_UNIXTIME(${timespan.to})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
|
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
|
||||||
@@ -920,6 +925,25 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all indexed blocks with missing coinbase addresses
|
||||||
|
*/
|
||||||
|
public async $getBlocksWithoutCoinbaseAddresses(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const [blocks] = await DB.query(`
|
||||||
|
SELECT height, hash, coinbase_addresses
|
||||||
|
FROM blocks
|
||||||
|
WHERE coinbase_addresses IS NULL AND
|
||||||
|
coinbase_address IS NOT NULL
|
||||||
|
ORDER BY height DESC
|
||||||
|
`);
|
||||||
|
return blocks;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get blocks with missing coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save indexed median fee to avoid recomputing it later
|
* Save indexed median fee to avoid recomputing it later
|
||||||
*
|
*
|
||||||
@@ -958,6 +982,44 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save coinbase addresses
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @param addresses
|
||||||
|
*/
|
||||||
|
public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE blocks SET coinbase_addresses = ?
|
||||||
|
WHERE hash = ?`,
|
||||||
|
[JSON.stringify(addresses), id]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot update block coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save pool
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @param poolId
|
||||||
|
*/
|
||||||
|
public async $savePool(id: string, poolId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE blocks SET pool_id = ?
|
||||||
|
WHERE hash = ?`,
|
||||||
|
[poolId, id]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a mysql row block into a BlockExtended. Note that you
|
* Convert a mysql row block into a BlockExtended. Note that you
|
||||||
* must provide the correct field into dbBlk object param
|
* must provide the correct field into dbBlk object param
|
||||||
@@ -997,6 +1059,7 @@ class BlocksRepository {
|
|||||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||||
extras.coinbaseRaw = dbBlk.coinbaseRaw;
|
extras.coinbaseRaw = dbBlk.coinbaseRaw;
|
||||||
extras.coinbaseAddress = dbBlk.coinbaseAddress;
|
extras.coinbaseAddress = dbBlk.coinbaseAddress;
|
||||||
|
extras.coinbaseAddresses = dbBlk.coinbaseAddresses ? JSON.parse(dbBlk.coinbaseAddresses) : [];
|
||||||
extras.coinbaseSignature = dbBlk.coinbaseSignature;
|
extras.coinbaseSignature = dbBlk.coinbaseSignature;
|
||||||
extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii;
|
extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii;
|
||||||
extras.avgTxSize = dbBlk.avgTxSize;
|
extras.avgTxSize = dbBlk.avgTxSize;
|
||||||
@@ -1043,7 +1106,7 @@ class BlocksRepository {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
|
|||||||
@@ -114,6 +114,43 @@ class BlocksSummariesRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT
|
||||||
|
height,
|
||||||
|
id,
|
||||||
|
version
|
||||||
|
FROM blocks_summaries
|
||||||
|
WHERE version < ?
|
||||||
|
ORDER BY height DESC;`, [version]);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT
|
||||||
|
blocks_summaries.height as height,
|
||||||
|
blocks_templates.id as id,
|
||||||
|
blocks_templates.version as version
|
||||||
|
FROM blocks_templates
|
||||||
|
JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id
|
||||||
|
WHERE blocks_templates.version < ?
|
||||||
|
ORDER BY height DESC;`, [version]);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the fee percentiles if the block has already been indexed, [] otherwise
|
* Get the fee percentiles if the block has already been indexed, [] otherwise
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -91,6 +91,26 @@ class CpfpRepository {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
|
||||||
|
const [clusterRows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE height = ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
return clusterRows.map(cluster => {
|
||||||
|
if (cluster?.txs) {
|
||||||
|
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
||||||
|
cluster.txs = this.unpack(cluster.txs);
|
||||||
|
return cluster;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(cluster => cluster !== null);
|
||||||
|
}
|
||||||
|
|
||||||
public async $deleteClustersFrom(height: number): Promise<void> {
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
try {
|
try {
|
||||||
@@ -122,6 +142,37 @@ class CpfpRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $deleteClustersAt(height: number): Promise<void> {
|
||||||
|
logger.info(`Delete cpfp clusters at height ${height} from the database`);
|
||||||
|
try {
|
||||||
|
const [rows] = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT txs, height, root from compact_cpfp_clusters
|
||||||
|
WHERE height = ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
) as RowDataPacket[][];
|
||||||
|
if (rows?.length) {
|
||||||
|
for (const clusterToDelete of rows) {
|
||||||
|
const txs = this.unpack(clusterToDelete?.txs);
|
||||||
|
for (const tx of txs) {
|
||||||
|
await transactionRepository.$removeTransaction(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE from compact_cpfp_clusters
|
||||||
|
WHERE height = ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// insert a dummy row to mark that we've indexed as far as this block
|
// insert a dummy row to mark that we've indexed as far as this block
|
||||||
public async $insertProgressMarker(height: number): Promise<void> {
|
public async $insertProgressMarker(height: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -190,6 +241,32 @@ class CpfpRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns `true` if two sets of CPFP clusters are deeply identical
|
||||||
|
public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean {
|
||||||
|
if (clustersA.length !== clustersB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root));
|
||||||
|
clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root));
|
||||||
|
|
||||||
|
for (let i = 0; i < clustersA.length; i++) {
|
||||||
|
if (clustersA[i].root !== clustersB[i].root) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (clustersA[i].txs.length !== clustersB[i].txs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let j = 0; j < clustersA[i].txs.length; j++) {
|
||||||
|
if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CpfpRepository();
|
export default new CpfpRepository();
|
||||||
@@ -50,10 +50,10 @@ class PoolsUpdater {
|
|||||||
|
|
||||||
// See backend README for more details about the mining pools update process
|
// See backend README for more details about the mining pools update process
|
||||||
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
|
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
|
||||||
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
|
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
||||||
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
||||||
) {
|
) {
|
||||||
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_BLOCK_REINDEXING is disabled`);
|
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
|
||||||
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
|
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import config from '../../config';
|
||||||
import { query } from '../../utils/axios-query';
|
import { query } from '../../utils/axios-query';
|
||||||
import { ConversionFeed, ConversionRates } from '../price-updater';
|
import { ConversionFeed, ConversionRates } from '../price-updater';
|
||||||
|
|
||||||
@@ -37,15 +38,26 @@ const emptyRates = {
|
|||||||
ZAR: -1,
|
ZAR: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FreeCurrencyApi implements ConversionFeed {
|
type PaidCurrencyData = {
|
||||||
private API_KEY: string;
|
[key: string]: {
|
||||||
|
code: string;
|
||||||
constructor(apiKey: string) {
|
value: number;
|
||||||
this.API_KEY = apiKey;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type FreeCurrencyData = {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FreeCurrencyApi implements ConversionFeed {
|
||||||
|
private API_KEY = config.FIAT_PRICE.API_KEY;
|
||||||
|
private PAID = config.FIAT_PRICE.PAID;
|
||||||
|
private API_URL_PREFIX: string = this.PAID ? `https://api.currencyapi.com/v3/` : `https://api.freecurrencyapi.com/v1/`;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
public async $getQuota(): Promise<any> {
|
public async $getQuota(): Promise<any> {
|
||||||
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
|
const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`);
|
||||||
if (response && response['quotas']) {
|
if (response && response['quotas']) {
|
||||||
return response['quotas'];
|
return response['quotas'];
|
||||||
}
|
}
|
||||||
@@ -53,21 +65,36 @@ class FreeCurrencyApi implements ConversionFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
|
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
|
||||||
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
|
const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`);
|
||||||
if (response && response['data']) {
|
if (response && response['data']) {
|
||||||
|
if (this.PAID) {
|
||||||
|
response['data'] = this.convertData(response['data']);
|
||||||
|
}
|
||||||
return response['data'];
|
return response['data'];
|
||||||
}
|
}
|
||||||
return emptyRates;
|
return emptyRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
|
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
|
||||||
const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`);
|
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true);
|
||||||
if (response && response['data'] && response['data'][date]) {
|
if (response && response['data'] && (response['data'][date] || this.PAID)) {
|
||||||
|
if (this.PAID) {
|
||||||
|
response['data'] = this.convertData(response['data']);
|
||||||
|
response['data'][response['meta'].last_updated_at.substr(0, 10)] = response['data'];
|
||||||
|
}
|
||||||
return response['data'][date];
|
return response['data'][date];
|
||||||
}
|
}
|
||||||
return emptyRates;
|
return emptyRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private convertData(data: PaidCurrencyData): FreeCurrencyData {
|
||||||
|
const simplifiedData: FreeCurrencyData = {};
|
||||||
|
for (const key in data) {
|
||||||
|
simplifiedData[key] = data[key].value;
|
||||||
|
}
|
||||||
|
return simplifiedData;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FreeCurrencyApi;
|
export default FreeCurrencyApi;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class PriceUpdater {
|
|||||||
private currencyConversionFeed: ConversionFeed | undefined;
|
private currencyConversionFeed: ConversionFeed | undefined;
|
||||||
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
|
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
|
||||||
private lastTimeConversionsRatesFetched: number = 0;
|
private lastTimeConversionsRatesFetched: number = 0;
|
||||||
private latestConversionsRatesFromFeed: ConversionRates = {};
|
private latestConversionsRatesFromFeed: ConversionRates = { USD: -1 };
|
||||||
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -71,7 +71,7 @@ class PriceUpdater {
|
|||||||
this.feeds.push(new BitfinexApi());
|
this.feeds.push(new BitfinexApi());
|
||||||
this.feeds.push(new GeminiApi());
|
this.feeds.push(new GeminiApi());
|
||||||
|
|
||||||
this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY);
|
this.currencyConversionFeed = new FreeCurrencyApi();
|
||||||
this.setCyclePosition();
|
this.setCyclePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,9 +157,9 @@ class PriceUpdater {
|
|||||||
try {
|
try {
|
||||||
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
|
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
|
||||||
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
|
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
|
||||||
logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
logger.debug(`Fetched currencies conversion rates from conversions API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Cannot fetch conversion rates from conversions API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,17 +408,17 @@ class PriceUpdater {
|
|||||||
try {
|
try {
|
||||||
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
|
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
|
||||||
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
|
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
|
||||||
logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
logger.debug(`Not enough conversions API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
||||||
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
|
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Cannot fetch conversions API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.additionalCurrenciesHistoryRunning = true;
|
this.additionalCurrenciesHistoryRunning = true;
|
||||||
logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
logger.debug(`Inserting missing historical conversion rates using conversions API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
||||||
|
|
||||||
let conversionRates: { [timestamp: number]: ConversionRates } = {};
|
let conversionRates: { [timestamp: number]: ConversionRates } = {};
|
||||||
let totalInserted = 0;
|
let totalInserted = 0;
|
||||||
@@ -430,10 +430,23 @@ class PriceUpdater {
|
|||||||
const month = new Date(priceTime.time * 1000).getMonth();
|
const month = new Date(priceTime.time * 1000).getMonth();
|
||||||
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
|
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
|
||||||
if (conversionRates[yearMonthTimestamp] === undefined) {
|
if (conversionRates[yearMonthTimestamp] === undefined) {
|
||||||
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 };
|
try {
|
||||||
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
if (year === new Date().getFullYear() && month === new Date().getMonth()) { // For rows in the current month, we use the latest conversion rates
|
||||||
logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining);
|
conversionRates[yearMonthTimestamp] = this.latestConversionsRatesFromFeed;
|
||||||
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
} else {
|
||||||
|
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-15`) || { USD: -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
||||||
|
throw new Error('Incorrect USD conversion rate');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if ((e instanceof Error ? e.message : '').includes('429')) { // Continue 60 seconds later if and only if error is 429
|
||||||
|
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||||
|
logger.info(`Got a 429 error from conversions API. This is expected to happen a few times during the initial historical price insertion, process will resume in 60 seconds.`, logger.tags.mining);
|
||||||
|
} else {
|
||||||
|
logger.err(`Cannot fetch conversion rates from conversions API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01, trying again next day. Error: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
backend/src/utils/api.ts
Normal file
9
backend/src/utils/api.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
|
||||||
|
if (req.accepts('json')) {
|
||||||
|
res.status(statusCode).json({ error: errorMessage });
|
||||||
|
} else {
|
||||||
|
res.status(statusCode).send(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import config from '../config';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
|
||||||
export async function query(path): Promise<object | undefined> {
|
export async function query(path, throwOnFail: boolean = false): Promise<object | undefined> {
|
||||||
type axiosOptions = {
|
type axiosOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': string
|
'User-Agent': string
|
||||||
@@ -21,6 +21,7 @@ export async function query(path): Promise<object | undefined> {
|
|||||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
};
|
};
|
||||||
let retry = 0;
|
let retry = 0;
|
||||||
|
let lastError: any = null;
|
||||||
|
|
||||||
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +51,7 @@ export async function query(path): Promise<object | undefined> {
|
|||||||
}
|
}
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
|
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
retry++;
|
retry++;
|
||||||
}
|
}
|
||||||
@@ -59,5 +61,10 @@ export async function query(path): Promise<object | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
|
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
|
||||||
|
|
||||||
|
if (throwOnFail && lastError) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
if (!opN) {
|
if (!opN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
if (!opM) {
|
if (!opM) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
|
|||||||
3
contributors/bitcoinmechanic.txt
Normal file
3
contributors/bitcoinmechanic.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||||
|
|
||||||
|
Signed: bitcoinmechanic
|
||||||
3
contributors/daweilv.txt
Normal file
3
contributors/daweilv.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 2024.
|
||||||
|
|
||||||
|
Signed: daweilv
|
||||||
3
contributors/hans-crypto.txt
Normal file
3
contributors/hans-crypto.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 21, 2024.
|
||||||
|
|
||||||
|
Signed: hans-crypto
|
||||||
3
contributors/henrialb.txt
Normal file
3
contributors/henrialb.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 12, 2024.
|
||||||
|
|
||||||
|
Signed: henrialb
|
||||||
3
contributors/jlopp.txt
Normal file
3
contributors/jlopp.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 12, 2024.
|
||||||
|
|
||||||
|
Signed: jlopp
|
||||||
3
contributors/mackalex.txt
Normal file
3
contributors/mackalex.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 18th, 2024.
|
||||||
|
|
||||||
|
Signed: mackalex
|
||||||
3
contributors/svrgnty.txt
Normal file
3
contributors/svrgnty.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 9, 2024.
|
||||||
|
|
||||||
|
Signed: svrgnty
|
||||||
@@ -106,7 +106,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
"EXTERNAL_ASSETS": [],
|
"EXTERNAL_ASSETS": [],
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "info",
|
"STDOUT_LOG_MIN_PRIORITY": "info",
|
||||||
"INDEXING_BLOCKS_AMOUNT": false,
|
"INDEXING_BLOCKS_AMOUNT": false,
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
@@ -137,7 +137,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_EXTERNAL_ASSETS: ""
|
MEMPOOL_EXTERNAL_ASSETS: ""
|
||||||
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
|
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
|
||||||
MEMPOOL_INDEXING_BLOCKS_AMOUNT: ""
|
MEMPOOL_INDEXING_BLOCKS_AMOUNT: ""
|
||||||
MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: ""
|
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
||||||
MEMPOOL_POOLS_JSON_URL: ""
|
MEMPOOL_POOLS_JSON_URL: ""
|
||||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||||
MEMPOOL_CPFP_INDEXING: ""
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.12.0-buster-slim AS builder
|
FROM node:20.15.0-buster-slim AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||||
@@ -24,7 +24,7 @@ RUN npm install --omit=dev --omit=optional
|
|||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN npm run package
|
RUN npm run package
|
||||||
|
|
||||||
FROM node:20.12.0-buster-slim
|
FROM node:20.15.0-buster-slim
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
"OFFICIAL": __MEMPOOL_OFFICIAL__,
|
||||||
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
|
||||||
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
|
||||||
|
"UNIX_SOCKET_PATH": "__MEMPOOL_UNIX_SOCKET_PATH__",
|
||||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
||||||
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
||||||
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
|
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
|
"AUTOMATIC_POOLS_UPDATE": __MEMPOOL_AUTOMATIC_POOLS_UPDATE__,
|
||||||
"AUDIT": __MEMPOOL_AUDIT__,
|
"AUDIT": __MEMPOOL_AUDIT__,
|
||||||
"RUST_GBT": __MEMPOOL_RUST_GBT__,
|
"RUST_GBT": __MEMPOOL_RUST_GBT__,
|
||||||
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
|
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
|
||||||
@@ -59,7 +60,8 @@
|
|||||||
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
|
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
|
||||||
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
|
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
|
||||||
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
|
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
|
||||||
"FALLBACK": __ESPLORA_FALLBACK__
|
"FALLBACK": __ESPLORA_FALLBACK__,
|
||||||
|
"MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||||
@@ -136,6 +138,8 @@
|
|||||||
"ENABLED": __REPLICATION_ENABLED__,
|
"ENABLED": __REPLICATION_ENABLED__,
|
||||||
"AUDIT": __REPLICATION_AUDIT__,
|
"AUDIT": __REPLICATION_AUDIT__,
|
||||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||||
|
"STATISTICS": __REPLICATION_STATISTICS__,
|
||||||
|
"STATISTICS_START_TIME": __REPLICATION_STATISTICS_START_TIME__,
|
||||||
"SERVERS": __REPLICATION_SERVERS__
|
"SERVERS": __REPLICATION_SERVERS__
|
||||||
},
|
},
|
||||||
"MEMPOOL_SERVICES": {
|
"MEMPOOL_SERVICES": {
|
||||||
@@ -149,6 +153,7 @@
|
|||||||
},
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": __FIAT_PRICE_ENABLED__,
|
"ENABLED": __FIAT_PRICE_ENABLED__,
|
||||||
|
"PAID": __FIAT_PRICE_PAID__,
|
||||||
"API_KEY": "__FIAT_PRICE_API_KEY__"
|
"API_KEY": "__FIAT_PRICE_API_KEY__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
|
|||||||
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
|
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
|
||||||
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
|
||||||
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||||
|
__MEMPOOL_UNIX_SOCKET_PATH__=${MEMPOOL_UNIX_SOCKET_PATH:=""}
|
||||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||||
@@ -25,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
|||||||
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||||
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||||
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
|
||||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||||
@@ -61,6 +62,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
|
|||||||
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
|
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
|
||||||
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
|
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
|
||||||
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
|
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
|
||||||
|
__ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2}
|
||||||
|
|
||||||
# SECOND_CORE_RPC
|
# SECOND_CORE_RPC
|
||||||
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
|
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
|
||||||
@@ -134,22 +136,25 @@ __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mm
|
|||||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
||||||
|
|
||||||
# REPLICATION
|
# REPLICATION
|
||||||
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=true}
|
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false}
|
||||||
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false}
|
||||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||||
|
__REPLICATION_STATISTICS__=${REPLICATION_STATISTICS:=false}
|
||||||
|
__REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=1481932800}
|
||||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||||
|
|
||||||
# MEMPOOL_SERVICES
|
# MEMPOOL_SERVICES
|
||||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
||||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||||
|
|
||||||
# FIAT_PRICE
|
# FIAT_PRICE
|
||||||
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
|
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
|
||||||
|
__FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false}
|
||||||
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
|
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
|
||||||
|
|
||||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||||
@@ -160,6 +165,7 @@ sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
|
|||||||
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_UNIX_SOCKET_PATH__!${__MEMPOOL_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||||
@@ -178,7 +184,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
|
|||||||
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||||
@@ -211,6 +217,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE
|
|||||||
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
|
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
|
||||||
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
|
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
|
||||||
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
|
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
|
||||||
|
sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
|
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
|
||||||
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
|
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
|
||||||
@@ -281,6 +288,8 @@ sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.jso
|
|||||||
sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json
|
||||||
sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REPLICATION_STATISTICS__!${__REPLICATION_STATISTICS__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REPLICATION_STATISTICS_START_TIME__!${__REPLICATION_STATISTICS_START_TIME__}!g" mempool-config.json
|
||||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||||
|
|
||||||
# MEMPOOL_SERVICES
|
# MEMPOOL_SERVICES
|
||||||
@@ -294,6 +303,7 @@ sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g"
|
|||||||
|
|
||||||
# FIAT_PRICE
|
# FIAT_PRICE
|
||||||
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
|
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
|
||||||
|
sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json
|
||||||
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
|
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
|
||||||
|
|
||||||
node /backend/package/index.js
|
node /backend/package/index.js
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.12.0-buster-slim AS builder
|
FROM node:20.15.0-buster-slim AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||||
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
|||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.25.4-alpine
|
FROM nginx:1.27.0-alpine
|
||||||
|
|
||||||
WORKDIR /patch
|
WORKDIR /patch
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ fi
|
|||||||
|
|
||||||
# Runtime overrides - read env vars defined in docker compose
|
# Runtime overrides - read env vars defined in docker compose
|
||||||
|
|
||||||
|
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
|
||||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||||
|
__TESTNET4_ENABLED__=${TESTNET_ENABLED:=false}
|
||||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||||
@@ -28,6 +30,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999}
|
|||||||
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
||||||
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
||||||
|
__ROOT_NETWORK__=${ROOT_NETWORK:=}
|
||||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
||||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
||||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||||
@@ -37,12 +40,16 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
|||||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||||
|
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||||
|
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||||
|
|
||||||
# Export as environment variables to be used by envsubst
|
# Export as environment variables to be used by envsubst
|
||||||
|
export __MAINNET_ENABLED__
|
||||||
export __TESTNET_ENABLED__
|
export __TESTNET_ENABLED__
|
||||||
|
export __TESTNET4_ENABLED__
|
||||||
export __SIGNET_ENABLED__
|
export __SIGNET_ENABLED__
|
||||||
export __LIQUID_ENABLED__
|
export __LIQUID_ENABLED__
|
||||||
export __LIQUID_TESTNET_ENABLED__
|
export __LIQUID_TESTNET_ENABLED__
|
||||||
@@ -54,6 +61,7 @@ export __NGINX_PORT__
|
|||||||
export __BLOCK_WEIGHT_UNITS__
|
export __BLOCK_WEIGHT_UNITS__
|
||||||
export __MEMPOOL_BLOCKS_AMOUNT__
|
export __MEMPOOL_BLOCKS_AMOUNT__
|
||||||
export __BASE_MODULE__
|
export __BASE_MODULE__
|
||||||
|
export __ROOT_NETWORK__
|
||||||
export __MEMPOOL_WEBSITE_URL__
|
export __MEMPOOL_WEBSITE_URL__
|
||||||
export __LIQUID_WEBSITE_URL__
|
export __LIQUID_WEBSITE_URL__
|
||||||
export __MINING_DASHBOARD__
|
export __MINING_DASHBOARD__
|
||||||
@@ -63,6 +71,8 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
|||||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||||
export __ACCELERATOR__
|
export __ACCELERATOR__
|
||||||
|
export __ACCELERATOR_BUTTON__
|
||||||
|
export __SERVICES_API__
|
||||||
export __PUBLIC_ACCELERATIONS__
|
export __PUBLIC_ACCELERATIONS__
|
||||||
export __HISTORICAL_PRICE__
|
export __HISTORICAL_PRICE__
|
||||||
export __ADDITIONAL_CURRENCIES__
|
export __ADDITIONAL_CURRENCIES__
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
"prefer-rest-params": 1,
|
"prefer-rest-params": 1,
|
||||||
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
||||||
"semi": 1,
|
"semi": 1,
|
||||||
"eqeqeq": 1
|
"curly": [1, "all"],
|
||||||
|
"eqeqeq": 1,
|
||||||
|
"no-trailing-spaces": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -63,6 +63,7 @@ src/resources/pools.json
|
|||||||
src/resources/mining-pools/*
|
src/resources/mining-pools/*
|
||||||
src/resources/**/*.mp4
|
src/resources/**/*.mp4
|
||||||
src/resources/**/*.vtt
|
src/resources/**/*.vtt
|
||||||
|
src/resources/customize.js
|
||||||
|
|
||||||
# environment config
|
# environment config
|
||||||
mempool-frontend-config.json
|
mempool-frontend-config.json
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
|||||||
|
|
||||||
### 3. Run the Frontend
|
### 3. Run the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Install project dependencies and run the frontend server:
|
Install project dependencies and run the frontend server:
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
|||||||
|
|
||||||
### 1. Build the Frontend
|
### 1. Build the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Build the frontend:
|
Build the frontend:
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@
|
|||||||
"translation": "src/locale/messages.fr.xlf",
|
"translation": "src/locale/messages.fr.xlf",
|
||||||
"baseHref": "/fr/"
|
"baseHref": "/fr/"
|
||||||
},
|
},
|
||||||
|
"hr": {
|
||||||
|
"translation": "src/locale/messages.hr.xlf",
|
||||||
|
"baseHref": "/hr/"
|
||||||
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"translation": "src/locale/messages.ja.xlf",
|
"translation": "src/locale/messages.ja.xlf",
|
||||||
"baseHref": "/ja/"
|
"baseHref": "/ja/"
|
||||||
@@ -166,10 +170,26 @@
|
|||||||
"src/resources",
|
"src/resources",
|
||||||
"src/robots.txt",
|
"src/robots.txt",
|
||||||
"src/config.js",
|
"src/config.js",
|
||||||
|
"src/customize.js",
|
||||||
"src/config.template.js"
|
"src/config.template.js"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss",
|
"src/styles.scss",
|
||||||
|
{
|
||||||
|
"input": "src/theme-contrast.scss",
|
||||||
|
"bundleName": "contrast",
|
||||||
|
"inject": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme-wiz.scss",
|
||||||
|
"bundleName": "wiz",
|
||||||
|
"inject": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme-bukele.scss",
|
||||||
|
"bundleName": "bukele",
|
||||||
|
"inject": false
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||||
],
|
],
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
|
|||||||
52
frontend/custom-sv-config.json
Normal file
52
frontend/custom-sv-config.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"theme": "bukele",
|
||||||
|
"enterprise": "onbtc",
|
||||||
|
"branding": {
|
||||||
|
"name": "onbtc",
|
||||||
|
"title": "Bitcoin Office",
|
||||||
|
"site_id": 19,
|
||||||
|
"header_img": "/resources/onbtclogo.svg",
|
||||||
|
"footer_img": "/resources/onbtclogo.svg",
|
||||||
|
"rounded_corner": true
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"component": "fees",
|
||||||
|
"mobileOrder": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "balance",
|
||||||
|
"mobileOrder": 1,
|
||||||
|
"props": {
|
||||||
|
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "twitter",
|
||||||
|
"mobileOrder": 5,
|
||||||
|
"props": {
|
||||||
|
"handle": "nayibbukele"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "address",
|
||||||
|
"mobileOrder": 2,
|
||||||
|
"props": {
|
||||||
|
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
|
||||||
|
"period": "1m"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "addressTransactions",
|
||||||
|
"mobileOrder": 3,
|
||||||
|
"props": {
|
||||||
|
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ describe('Liquid', () => {
|
|||||||
|
|
||||||
it('loads a specific block page', () => {
|
it('loads a specific block page', () => {
|
||||||
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
|
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,20 +72,6 @@ describe('Liquid', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders unconfidential addresses correctly on mobile', () => {
|
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`);
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
//TODO: Add proper IDs for these selectors
|
|
||||||
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
|
|
||||||
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
|
|
||||||
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
|
|
||||||
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
|
|
||||||
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('peg in/peg out', () => {
|
describe('peg in/peg out', () => {
|
||||||
it('loads peg in addresses', () => {
|
it('loads peg in addresses', () => {
|
||||||
cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`);
|
cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`);
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ describe('Liquid Testnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads a specific block page', () => {
|
it('loads a specific block page', () => {
|
||||||
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
|
cy.visit(`${basePath}/block/fb4cbcbff3993ca4bf8caf657d55a23db5ed4ab1cfa33c489303c2e04e1c38e0`);
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('[id^="bitcoin-block-"]').should('have.length', 22);
|
cy.get('[id^="bitcoin-block-"]').should('have.length', 22);
|
||||||
cy.get('.footer').should('be.visible');
|
cy.get('.footer').should('be.visible');
|
||||||
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
||||||
expect(text).to.match(/Incoming transactions.* vB\/s/);
|
expect(text).to.match(/Incoming Transactions.* vB\/s/);
|
||||||
});
|
});
|
||||||
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
||||||
expect(text).to.match(/Unconfirmed:(.*)/);
|
expect(text).to.match(/Unconfirmed:(.*)/);
|
||||||
@@ -103,6 +103,7 @@ describe('Mainnet', () => {
|
|||||||
|
|
||||||
it('check op_return tx tooltip', () => {
|
it('check op_return tx tooltip', () => {
|
||||||
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
|
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
|
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
|
||||||
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
|
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
|
||||||
@@ -111,9 +112,10 @@ describe('Mainnet', () => {
|
|||||||
|
|
||||||
it('check op_return coinbase tooltip', () => {
|
it('check op_return coinbase tooltip', () => {
|
||||||
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
|
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('div > a > .badge').first().trigger('onmouseover');
|
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
|
||||||
cy.get('div > a > .badge').first().trigger('mouseenter');
|
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
|
||||||
cy.get('.tooltip-inner').should('be.visible');
|
cy.get('.tooltip-inner').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,13 +144,13 @@ describe('Mainnet', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
|
['BC1PQYQS', 'bc1PqYqS'].forEach((searchTerm) => {
|
||||||
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
|
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
||||||
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
|
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
|
||||||
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
|
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
|
||||||
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e');
|
cy.url().should('include', '/address/bc1pqyqs26fs4gnyw4aqttyjqa5ta7075zzfjftyz98qa8vdr49dh7fqm2zkv3');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
});
|
});
|
||||||
@@ -156,13 +158,13 @@ describe('Mainnet', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => {
|
['BC1Q0003', 'bC1q0003'].forEach((searchTerm) => {
|
||||||
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
|
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
||||||
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
|
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
|
||||||
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
|
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
|
||||||
cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy');
|
cy.url().should('include', '/address/bc1q000303cgr9zazthut63kdktwtatfe206um8nyh');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
});
|
});
|
||||||
@@ -283,6 +285,7 @@ describe('Mainnet', () => {
|
|||||||
it('loads genesis block and keypress arrow right', () => {
|
it('loads genesis block and keypress arrow right', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.viewport('macbook-16');
|
||||||
cy.visit('/block/0');
|
cy.visit('/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
@@ -295,6 +298,7 @@ describe('Mainnet', () => {
|
|||||||
it('loads genesis block and keypress arrow left', () => {
|
it('loads genesis block and keypress arrow left', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.viewport('macbook-16');
|
||||||
cy.visit('/block/0');
|
cy.visit('/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
@@ -323,6 +327,7 @@ describe('Mainnet', () => {
|
|||||||
it('loads genesis block and click on the arrow left', () => {
|
it('loads genesis block and click on the arrow left', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.viewport('macbook-16');
|
||||||
cy.visit('/block/0');
|
cy.visit('/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
@@ -339,7 +344,7 @@ describe('Mainnet', () => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
cy.changeNetwork('testnet');
|
cy.changeNetwork('testnet4');
|
||||||
cy.changeNetwork('signet');
|
cy.changeNetwork('signet');
|
||||||
cy.changeNetwork('mainnet');
|
cy.changeNetwork('mainnet');
|
||||||
});
|
});
|
||||||
@@ -439,6 +444,7 @@ describe('Mainnet', () => {
|
|||||||
describe('blocks', () => {
|
describe('blocks', () => {
|
||||||
it('shows empty blocks properly', () => {
|
it('shows empty blocks properly', () => {
|
||||||
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
@@ -446,6 +452,7 @@ describe('Mainnet', () => {
|
|||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
it('expands and collapses the block details', () => {
|
||||||
cy.visit('/block/0');
|
cy.visit('/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
@@ -458,6 +465,7 @@ describe('Mainnet', () => {
|
|||||||
});
|
});
|
||||||
it('shows blocks with no pagination', () => {
|
it('shows blocks with no pagination', () => {
|
||||||
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
|
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
|
||||||
@@ -467,6 +475,7 @@ describe('Mainnet', () => {
|
|||||||
it('supports pagination on the block screen', () => {
|
it('supports pagination on the block screen', () => {
|
||||||
// 41 txs
|
// 41 txs
|
||||||
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.pagination-container a').invoke('text').then((text1) => {
|
cy.get('.pagination-container a').invoke('text').then((text1) => {
|
||||||
cy.get('.active + li').first().click().then(() => {
|
cy.get('.active + li').first().click().then(() => {
|
||||||
@@ -482,6 +491,7 @@ describe('Mainnet', () => {
|
|||||||
it('shows blocks pagination with 5 pages (desktop)', () => {
|
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||||
cy.viewport(760, 800);
|
cy.viewport(760, 800);
|
||||||
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
});
|
});
|
||||||
@@ -493,6 +503,7 @@ describe('Mainnet', () => {
|
|||||||
it('shows blocks pagination with 3 pages (mobile)', () => {
|
it('shows blocks pagination with 3 pages (mobile)', () => {
|
||||||
cy.viewport(669, 800);
|
cy.viewport(669, 800);
|
||||||
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
});
|
});
|
||||||
@@ -532,16 +543,7 @@ describe('Mainnet', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('.alert').should('be.visible');
|
cy.get('.alert-replaced').should('be.visible');
|
||||||
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
|
|
||||||
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
|
|
||||||
cy.get('.alert').then(getRectangle).then((rectB) => {
|
|
||||||
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows RBF transactions properly (desktop)', () => {
|
it('shows RBF transactions properly (desktop)', () => {
|
||||||
@@ -592,4 +594,63 @@ describe('Mainnet', () => {
|
|||||||
} else {
|
} else {
|
||||||
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('Accelerated Transactions', () => {
|
||||||
|
describe('Unconfirmed Accelerated Transaction', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.intercept('/api/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
fixture: 'accelerated_tx.json'
|
||||||
|
}).as('tx');
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/cpfp/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
fixture: 'accelerated_cpfp.json'
|
||||||
|
}).as('accelerated_cpfp');
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/transaction-times?txId%5B%5D=40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
body: '[1723416086]',
|
||||||
|
}).as('transaction-time');
|
||||||
|
|
||||||
|
cy.intercept('https://mempool.space/api/v1/services/accelerator/accelerations/history', {
|
||||||
|
fixture: 'accelerated_history.json'
|
||||||
|
}).as('history');
|
||||||
|
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit('/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
command: 'txPosition'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows unconfirmed accelerated transaction properly', () => {
|
||||||
|
cy.get('.badge-accelerated').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"]').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"] > table > tbody > :nth-child(1) .oobFees').invoke('text').should('contain', `15.5 `);
|
||||||
|
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `3,000`);
|
||||||
|
cy.get('#acceleration-timeline').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// currently doesn't work due to 'accelerations/history' endpoint not being intercepted
|
||||||
|
it.skip('properly render accelerated transacion as it confirms', () => {
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
command: 'txPositionConfirmed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('.badge-accelerated').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"]').should('not.exist');
|
||||||
|
cy.get('[data-cy="fee-rate"]').invoke('text').should('contain', `2.17 `);
|
||||||
|
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `39`);
|
||||||
|
cy.get('#acceleration-timeline').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,12 +95,14 @@ describe('Signet', () => {
|
|||||||
describe('blocks', () => {
|
describe('blocks', () => {
|
||||||
it('shows empty blocks properly', () => {
|
it('shows empty blocks properly', () => {
|
||||||
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
|
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
it('expands and collapses the block details', () => {
|
||||||
cy.visit('/signet/block/0');
|
cy.visit('/signet/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
cy.get('#details').should('be.visible');
|
cy.get('#details').should('be.visible');
|
||||||
@@ -113,6 +115,7 @@ describe('Signet', () => {
|
|||||||
|
|
||||||
it('shows blocks with no pagination', () => {
|
it('shows blocks with no pagination', () => {
|
||||||
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
|
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('h2').invoke('text').should('equal', '13 transactions');
|
cy.get('h2').invoke('text').should('equal', '13 transactions');
|
||||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
@@ -121,6 +124,7 @@ describe('Signet', () => {
|
|||||||
it('supports pagination on the block screen', () => {
|
it('supports pagination on the block screen', () => {
|
||||||
// 43 txs
|
// 43 txs
|
||||||
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
|
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
cy.get('.active + li').first().click().then(() => {
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { emitMempoolInfo } from '../../support/websocket';
|
|||||||
|
|
||||||
const baseModule = Cypress.env('BASE_MODULE');
|
const baseModule = Cypress.env('BASE_MODULE');
|
||||||
|
|
||||||
describe('Testnet', () => {
|
describe('Testnet4', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('/api/block-height/*').as('block-height');
|
cy.intercept('/api/block-height/*').as('block-height');
|
||||||
cy.intercept('/api/block/*').as('block');
|
cy.intercept('/api/block/*').as('block');
|
||||||
@@ -13,7 +13,7 @@ describe('Testnet', () => {
|
|||||||
if (baseModule === 'mempool') {
|
if (baseModule === 'mempool') {
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
it('loads the dashboard', () => {
|
||||||
cy.visit('/testnet');
|
cy.visit('/testnet4');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe('Testnet', () => {
|
|||||||
|
|
||||||
it.skip('loads the dashboard with the skeleton blocks', () => {
|
it.skip('loads the dashboard with the skeleton blocks', () => {
|
||||||
cy.mockMempoolSocket();
|
cy.mockMempoolSocket();
|
||||||
cy.visit('/testnet');
|
cy.visit('/testnet4');
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
@@ -45,7 +45,7 @@ describe('Testnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads the pools screen', () => {
|
it('loads the pools screen', () => {
|
||||||
cy.visit('/testnet');
|
cy.visit('/testnet4');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#btn-pools').click().then(() => {
|
cy.get('#btn-pools').click().then(() => {
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
@@ -53,7 +53,7 @@ describe('Testnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads the graphs screen', () => {
|
it('loads the graphs screen', () => {
|
||||||
cy.visit('/testnet');
|
cy.visit('/testnet4');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#btn-graphs').click().then(() => {
|
cy.get('#btn-graphs').click().then(() => {
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
@@ -63,7 +63,7 @@ describe('Testnet', () => {
|
|||||||
describe('tv mode', () => {
|
describe('tv mode', () => {
|
||||||
it('loads the tv screen - desktop', () => {
|
it('loads the tv screen - desktop', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.viewport('macbook-16');
|
||||||
cy.visit('/testnet/graphs');
|
cy.visit('/testnet4/graphs');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#btn-tv').click().then(() => {
|
cy.get('#btn-tv').click().then(() => {
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
@@ -73,7 +73,7 @@ describe('Testnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads the tv screen - mobile', () => {
|
it('loads the tv screen - mobile', () => {
|
||||||
cy.visit('/testnet/graphs');
|
cy.visit('/testnet4/graphs');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#btn-tv').click().then(() => {
|
cy.get('#btn-tv').click().then(() => {
|
||||||
cy.viewport('iphone-6');
|
cy.viewport('iphone-6');
|
||||||
@@ -85,7 +85,7 @@ describe('Testnet', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
it('loads the api screen', () => {
|
||||||
cy.visit('/testnet');
|
cy.visit('/testnet4');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('#btn-docs').click().then(() => {
|
cy.get('#btn-docs').click().then(() => {
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
@@ -94,13 +94,15 @@ describe('Testnet', () => {
|
|||||||
|
|
||||||
describe('blocks', () => {
|
describe('blocks', () => {
|
||||||
it('shows empty blocks properly', () => {
|
it('shows empty blocks properly', () => {
|
||||||
cy.visit('/testnet/block/0');
|
cy.visit('/testnet4/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
it('expands and collapses the block details', () => {
|
||||||
cy.visit('/testnet/block/0');
|
cy.visit('/testnet4/block/0');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
cy.get('#details').should('be.visible');
|
cy.get('#details').should('be.visible');
|
||||||
@@ -112,15 +114,17 @@ describe('Testnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows blocks with no pagination', () => {
|
it('shows blocks with no pagination', () => {
|
||||||
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
|
cy.visit('/testnet4/block/000000000066e8b6cc78a93f8989587f5819624bae2eb1c05f535cadded19f99');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('h2').invoke('text').should('equal', '11 transactions');
|
cy.get('h2').invoke('text').should('equal', '18 transactions');
|
||||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports pagination on the block screen', () => {
|
it('supports pagination on the block screen', () => {
|
||||||
// 48 txs
|
// 48 txs
|
||||||
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
|
cy.visit('/testnet4/block/000000000000006982d53f8273bdff21dafc380c292eabc669b5ab6d732311c3');
|
||||||
|
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
cy.get('.active + li').first().click().then(() => {
|
cy.get('.active + li').first().click().then(() => {
|
||||||
20
frontend/cypress/fixtures/accelerated_cpfp.json
Normal file
20
frontend/cypress/fixtures/accelerated_cpfp.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"ancestors": [],
|
||||||
|
"bestDescendant": null,
|
||||||
|
"descendants": [],
|
||||||
|
"effectiveFeePerVsize": 15.452914798206278,
|
||||||
|
"sigops": 4,
|
||||||
|
"fee": 446,
|
||||||
|
"adjustedVsize": 223,
|
||||||
|
"acceleration": true,
|
||||||
|
"acceleratedBy": [
|
||||||
|
111,
|
||||||
|
43,
|
||||||
|
102,
|
||||||
|
112,
|
||||||
|
142,
|
||||||
|
115
|
||||||
|
],
|
||||||
|
"acceleratedAt": 1723417553,
|
||||||
|
"feeDelta": 3000
|
||||||
|
}
|
||||||
24
frontend/cypress/fixtures/accelerated_history.json
Normal file
24
frontend/cypress/fixtures/accelerated_history.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"status": "completed",
|
||||||
|
"added": 1723417553,
|
||||||
|
"lastUpdated": 1723424127,
|
||||||
|
"effectiveFee": 446,
|
||||||
|
"effectiveVsize": 223,
|
||||||
|
"feeDelta": 3000,
|
||||||
|
"blockHash": "000000000000000000005bc0a822da172e43c687428cc268177ad27d636f3059",
|
||||||
|
"blockHeight": 856387,
|
||||||
|
"bidBoost": 39,
|
||||||
|
"boostVersion": "v2",
|
||||||
|
"pools": [
|
||||||
|
111,
|
||||||
|
43,
|
||||||
|
102,
|
||||||
|
112,
|
||||||
|
142,
|
||||||
|
115
|
||||||
|
],
|
||||||
|
"minedByPoolUniqueId": 111
|
||||||
|
}
|
||||||
|
]
|
||||||
48
frontend/cypress/fixtures/accelerated_position.json
Normal file
48
frontend/cypress/fixtures/accelerated_position.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"txPosition": {
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"position": {
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"accelerated": true
|
||||||
|
},
|
||||||
|
"accelerationPositions": [
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 111,
|
||||||
|
"pool": "Foundry USA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 43,
|
||||||
|
"pool": "Braiins Pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 102,
|
||||||
|
"pool": "SpiderPool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 112,
|
||||||
|
"pool": "SBI Crypto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 142,
|
||||||
|
"pool": "OCEAN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 115,
|
||||||
|
"pool": "MARA Pool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"txConfirmed": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"block":{
|
||||||
|
"id": "000000000000000000014cc3d86b7c096ef92aca180e3cf27d72e34ce944caed",
|
||||||
|
"height": 837051,
|
||||||
|
"version": 821051392,
|
||||||
|
"timestamp": 1723452588,
|
||||||
|
"bits": 386079422,
|
||||||
|
"nonce": 2215159619,
|
||||||
|
"difficulty": 90666502495565.78,
|
||||||
|
"merkle_root": "207ad51f6c1150f63fcd043eb1b4624b77ac70558594317e989c1109fbb47c47",
|
||||||
|
"tx_count": 2284,
|
||||||
|
"size": 1490522,
|
||||||
|
"weight": 3993155,
|
||||||
|
"previousblockhash": "00000000000000000002b8a66307c997aa27bf99a384ceb7cfe5f29576eddb26",
|
||||||
|
"mediantime": 1723450608,
|
||||||
|
"stale": false,
|
||||||
|
"extras": {
|
||||||
|
"reward": 319417632,
|
||||||
|
"coinbaseRaw": "0378110d04adccb9662f466f756e6472792055534120506f6f6c202364726f70676f6c642f2c08727fca05000000000000",
|
||||||
|
"orphans": [],
|
||||||
|
"medianFee": 4.021446911342697,
|
||||||
|
"feeRange": [
|
||||||
|
3.1,
|
||||||
|
3.4184397163120566,
|
||||||
|
3.998624011007912,
|
||||||
|
4.444976076555024,
|
||||||
|
5.382978723404255,
|
||||||
|
11.62814371257485,
|
||||||
|
468.75
|
||||||
|
],
|
||||||
|
"totalFees": 6917632,
|
||||||
|
"avgFee": 3030,
|
||||||
|
"avgFeeRate": 6,
|
||||||
|
"utxoSetChange": -2647,
|
||||||
|
"avgTxSize": 652.44,
|
||||||
|
"totalInputs": 8544,
|
||||||
|
"totalOutputs": 5897,
|
||||||
|
"totalOutputAmt": 2950130527407,
|
||||||
|
"segwitTotalTxs": 2084,
|
||||||
|
"segwitTotalSize": 1137877,
|
||||||
|
"segwitTotalWeight": 2582683,
|
||||||
|
"feePercentiles": null,
|
||||||
|
"virtualSize": 998288.75,
|
||||||
|
"coinbaseAddress": "bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
|
||||||
|
"coinbaseAddresses": [
|
||||||
|
"bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
|
||||||
|
"bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj"
|
||||||
|
],
|
||||||
|
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 3daaca9b82a51aca960c1491588246029d7e0fc49e0abdbcc8fd17574be5c74b",
|
||||||
|
"coinbaseSignatureAscii": "f/Foundry USA Pool #dropgold/",
|
||||||
|
"header": "0040f03026dbed7695f2e5cfb7ce84a399bf27aa97c90763a6b802000000000000000000477cb4fb09119c987e3194855570ac774b62b4b13e04cd3ff650116c1fd57a20acccb966be1a031743a70884",
|
||||||
|
"utxoSetSize": null,
|
||||||
|
"totalInputAmt": null,
|
||||||
|
"pool": {
|
||||||
|
"id": 111,
|
||||||
|
"name": "Foundry USA",
|
||||||
|
"slug": "foundryusa"
|
||||||
|
},
|
||||||
|
"matchRate": 100,
|
||||||
|
"expectedFees": 6957093,
|
||||||
|
"expectedWeight": 3991895,
|
||||||
|
"similarity": 0.9907343565880212
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
frontend/cypress/fixtures/accelerated_tx.json
Normal file
45
frontend/cypress/fixtures/accelerated_tx.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"version": 1,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "7c6e17739d7225d097db1f08df17d06dc712dc0951f266db1070939b85b5e8e7",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
|
||||||
|
"value": 16610556
|
||||||
|
},
|
||||||
|
"scriptsig": "483045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01210275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01 OP_PUSHBYTES_33 0275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"scriptpubkey": "0014ce6c0bb00482016d12657174b6468cd01df6421e",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 ce6c0bb00482016d12657174b6468cd01df6421e",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qeekqhvqysgqk6yn9w96tv35v6qwlvss7vuvtj0",
|
||||||
|
"value": 6796193
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
|
||||||
|
"value": 9813917
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 223,
|
||||||
|
"weight": 892,
|
||||||
|
"sigops": 4,
|
||||||
|
"fee": 446,
|
||||||
|
"status": {
|
||||||
|
"confirmed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -750,7 +750,7 @@
|
|||||||
},
|
},
|
||||||
"backendInfo": {
|
"backendInfo": {
|
||||||
"hostname": "node205.tk7.mempool.space",
|
"hostname": "node205.tk7.mempool.space",
|
||||||
"version": "3.0.0-dev",
|
"version": "3.1.0-dev",
|
||||||
"gitCommit": "abbc8a134",
|
"gitCommit": "abbc8a134",
|
||||||
"lightning": false
|
"lightning": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Cypress.Commands.add('mockMempoolSocket', () => {
|
|||||||
mockWebSocket();
|
mockWebSocket();
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => {
|
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
|
||||||
cy.get('.dropdown-toggle').click().then(() => {
|
cy.get('.dropdown-toggle').click().then(() => {
|
||||||
cy.get(`a.${network}`).click().then(() => {
|
cy.get(`a.${network}`).click().then(() => {
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
|
|||||||
2
frontend/cypress/support/index.d.ts
vendored
2
frontend/cypress/support/index.d.ts
vendored
@@ -5,6 +5,6 @@ declare namespace Cypress {
|
|||||||
waitForSkeletonGone(): Chainable<any>
|
waitForSkeletonGone(): Chainable<any>
|
||||||
waitForPageIdle(): Chainable<any>
|
waitForPageIdle(): Chainable<any>
|
||||||
mockMempoolSocket(): Chainable<any>
|
mockMempoolSocket(): Chainable<any>
|
||||||
changeNetwork(network: "testnet"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +96,18 @@ export const emitMempoolInfo = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'txPosition': {
|
||||||
|
cy.readFile('cypress/fixtures/accelerated_position.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'txPositionConfirmed': {
|
||||||
|
cy.readFile('cypress/fixtures/accelerated_position_confirmed.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ const { spawnSync } = require('child_process');
|
|||||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||||
|
const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js';
|
||||||
|
|
||||||
let settings = [];
|
let settings = [];
|
||||||
let configContent = {};
|
let configContent = {};
|
||||||
let gitCommitHash = '';
|
let gitCommitHash = '';
|
||||||
let packetJsonVersion = '';
|
let packetJsonVersion = '';
|
||||||
|
let customConfig;
|
||||||
|
let customConfigContent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||||
@@ -22,7 +25,18 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
|
if (configContent && configContent.CUSTOMIZATION) {
|
||||||
|
try {
|
||||||
|
customConfig = readConfig(configContent.CUSTOMIZATION);
|
||||||
|
customConfigContent = JSON.parse(customConfig);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`failed to load customization config from ${configContent.CUSTOMIZATION}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseModuleName = configContent.BASE_MODULE || 'mempool';
|
||||||
|
const customBuildName = (customConfigContent && customConfigContent.enterprise) ? ('.' + customConfigContent.enterprise) : '';
|
||||||
|
const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.copyFileSync(indexFilePath, 'src/index.html');
|
fs.copyFileSync(indexFilePath, 'src/index.html');
|
||||||
@@ -109,6 +123,17 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
|||||||
|
|
||||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||||
|
|
||||||
|
let customConfigJs = '';
|
||||||
|
if (customConfig) {
|
||||||
|
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
|
||||||
|
customConfigJs = `(function (window) {
|
||||||
|
window.__env = window.__env || {};
|
||||||
|
window.__env.customize = ${customConfig};
|
||||||
|
}((typeof global !== 'undefined') ? global : this));
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
|
||||||
|
|
||||||
if (currentConfig && currentConfig === newConfig) {
|
if (currentConfig && currentConfig === newConfig) {
|
||||||
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"TESTNET_ENABLED": false,
|
"TESTNET_ENABLED": false,
|
||||||
|
"TESTNET4_ENABLED": false,
|
||||||
"SIGNET_ENABLED": false,
|
"SIGNET_ENABLED": false,
|
||||||
"LIQUID_ENABLED": false,
|
"LIQUID_ENABLED": false,
|
||||||
"LIQUID_TESTNET_ENABLED": false,
|
"LIQUID_TESTNET_ENABLED": false,
|
||||||
|
"MAINNET_ENABLED": true,
|
||||||
"ITEMS_PER_PAGE": 10,
|
"ITEMS_PER_PAGE": 10,
|
||||||
"KEEP_BLOCKS_AMOUNT": 8,
|
"KEEP_BLOCKS_AMOUNT": 8,
|
||||||
"NGINX_PROTOCOL": "http",
|
"NGINX_PROTOCOL": "http",
|
||||||
@@ -11,6 +13,7 @@
|
|||||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
"BASE_MODULE": "mempool",
|
"BASE_MODULE": "mempool",
|
||||||
|
"ROOT_NETWORK": "",
|
||||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
||||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||||
"MINING_DASHBOARD": true,
|
"MINING_DASHBOARD": true,
|
||||||
@@ -22,5 +25,7 @@
|
|||||||
"HISTORICAL_PRICE": true,
|
"HISTORICAL_PRICE": true,
|
||||||
"ADDITIONAL_CURRENCIES": false,
|
"ADDITIONAL_CURRENCIES": false,
|
||||||
"ACCELERATOR": false,
|
"ACCELERATOR": false,
|
||||||
"PUBLIC_ACCELERATIONS": false
|
"ACCELERATOR_BUTTON": true,
|
||||||
|
"PUBLIC_ACCELERATIONS": false,
|
||||||
|
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||||
}
|
}
|
||||||
|
|||||||
1115
frontend/package-lock.json
generated
1115
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user