Compare commits
146 Commits
mononaut/f
...
mononaut/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5682b157c1 | ||
|
|
77b0a8eccc | ||
|
|
6af6274f43 | ||
|
|
379fa3fe49 | ||
|
|
fe76a0fd08 | ||
|
|
412d83f961 | ||
|
|
9a99ee6486 | ||
|
|
fd30bff9c6 | ||
|
|
804640216f | ||
|
|
2191bf2a22 | ||
|
|
7d92cdf015 | ||
|
|
386037d1db | ||
|
|
f2e216b9ac | ||
|
|
eb8c38e4e8 | ||
|
|
689319437a | ||
|
|
9e1de656c1 | ||
|
|
37dd95a4a0 | ||
|
|
6076eeed46 | ||
|
|
8b1dff6d15 | ||
|
|
9d4b58604b | ||
|
|
c49626aefc | ||
|
|
baeb200e8b | ||
|
|
11669849ab | ||
|
|
b171ed6dd0 | ||
|
|
0ac76e12c4 | ||
|
|
ee1ec414ed | ||
|
|
0e5dc21854 | ||
|
|
bf7df08305 | ||
|
|
ec63c822db | ||
|
|
09e4e44e88 | ||
|
|
c5e6821ad4 | ||
|
|
70fa78b987 | ||
|
|
8c78e35063 | ||
|
|
126a75ed45 | ||
|
|
0703690190 | ||
|
|
7f56f0295b | ||
|
|
27154da191 | ||
|
|
0fb61fded3 | ||
|
|
ce00814bbf | ||
|
|
c758a3538b | ||
|
|
60fd2df4e6 | ||
|
|
05e38b43b9 | ||
|
|
9fa1eebc02 | ||
|
|
79ea1ee257 | ||
|
|
add405481e | ||
|
|
c382e03e4a | ||
|
|
ac73de31b5 | ||
|
|
ccd403b771 | ||
|
|
f28c460bb4 | ||
|
|
5711c939d7 | ||
|
|
ef3ce6d187 | ||
|
|
410e604dce | ||
|
|
8ffe2f3156 | ||
|
|
d27febf878 | ||
|
|
038f9659bb | ||
|
|
aeb896e200 | ||
|
|
349ba613dd | ||
|
|
714208a366 | ||
|
|
ad4c7d7c0b | ||
|
|
264c02eca1 | ||
|
|
ea42077320 | ||
|
|
92ffb441a1 | ||
|
|
725526d733 | ||
|
|
31ea2e1d4b | ||
|
|
c81509d762 | ||
|
|
07f16165ff | ||
|
|
536f29eb0d | ||
|
|
369db7a63c | ||
|
|
81ec54fcb3 | ||
|
|
64d6bda728 | ||
|
|
7ab05d815d | ||
|
|
50a865e54e | ||
|
|
2156fb2a83 | ||
|
|
4e1087801a | ||
|
|
4dec152df0 | ||
|
|
720c2b8807 | ||
|
|
b00e0b6a73 | ||
|
|
6333f3aa47 | ||
|
|
7b127ebe8b | ||
|
|
585309f9f0 | ||
|
|
184ca3c662 | ||
|
|
bfea535df1 | ||
|
|
65189d6f3e | ||
|
|
de434bb398 | ||
|
|
804485a526 | ||
|
|
3f8fefcb0e | ||
|
|
178da3df26 | ||
|
|
02b5fadf44 | ||
|
|
ea52d40a10 | ||
|
|
cbd4ea1aa8 | ||
|
|
f95c16a78e | ||
|
|
fa1cf7abb9 | ||
|
|
6a5afd7f95 | ||
|
|
0ddd9b2487 | ||
|
|
ee1e75f978 | ||
|
|
97c8ace8f7 | ||
|
|
a3ca79e995 | ||
|
|
962c9543d0 | ||
|
|
52bcef54be | ||
|
|
22030027b4 | ||
|
|
53a9093a70 | ||
|
|
6ef793f202 | ||
|
|
b227ff8e1b | ||
|
|
db6b9ffa6e | ||
|
|
5a0c809fa7 | ||
|
|
f6caed3ffd | ||
|
|
6141516eb2 | ||
|
|
3b295b5482 | ||
|
|
adc395fc3d | ||
|
|
0c52898010 | ||
|
|
4fe6a74d20 | ||
|
|
7fca18d7be | ||
|
|
208756bdd2 | ||
|
|
82a072bd87 | ||
|
|
6d76d11837 | ||
|
|
982f1e007a | ||
|
|
a4bf545993 | ||
|
|
fe51f6a504 | ||
|
|
f5d311ca44 | ||
|
|
b81dd99825 | ||
|
|
f08457aba7 | ||
|
|
c601e5dcb4 | ||
|
|
11b82b3459 | ||
|
|
d322665789 | ||
|
|
49529627f8 | ||
|
|
107746feec | ||
|
|
e5bef55d47 | ||
|
|
17dd02ed4e | ||
|
|
7bb34fe090 | ||
|
|
abbaee0274 | ||
|
|
3d1cd3193a | ||
|
|
be53cd8b48 | ||
|
|
4b20ea7232 | ||
|
|
ffd7831efc | ||
|
|
f8636d20c2 | ||
|
|
3b4dd7e633 | ||
|
|
ea101e65bb | ||
|
|
8a713c3880 | ||
|
|
42d5650bc0 | ||
|
|
5257716e1a | ||
|
|
47b95af8ae | ||
|
|
822934828a | ||
|
|
d7333ec858 | ||
|
|
1df2c89cdb | ||
|
|
6602bddb2b | ||
|
|
bca35600ff |
@@ -122,5 +122,9 @@
|
||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "https://mempool.space/api",
|
||||
"ACCELERATIONS": false
|
||||
}
|
||||
}
|
||||
|
||||
289
backend/package-lock.json
generated
289
backend/package-lock.json
generated
@@ -1485,6 +1485,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
|
||||
"integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2403,12 +2414,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base-x": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
|
||||
"integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
|
||||
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
|
||||
},
|
||||
"node_modules/bech32": {
|
||||
"version": "2.0.0",
|
||||
@@ -2424,18 +2432,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bitcoinjs-lib": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz",
|
||||
"integrity": "sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
|
||||
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bech32": "^2.0.0",
|
||||
"bip174": "^2.1.0",
|
||||
"bs58check": "^2.1.2",
|
||||
"create-hash": "^1.1.0",
|
||||
"ripemd160": "^2.0.2",
|
||||
"bs58check": "^3.0.1",
|
||||
"typeforce": "^1.11.3",
|
||||
"varuint-bitcoin": "^1.1.2",
|
||||
"wif": "^2.0.1"
|
||||
"varuint-bitcoin": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -2540,21 +2546,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bs58": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"dependencies": {
|
||||
"base-x": "^3.0.2"
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bs58check": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
|
||||
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
|
||||
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
|
||||
"dependencies": {
|
||||
"bs58": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"safe-buffer": "^5.1.2"
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bs58": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bser": {
|
||||
@@ -2668,15 +2673,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
|
||||
@@ -2783,18 +2779,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"dependencies": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"inherits": "^2.0.1",
|
||||
"md5.js": "^1.3.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"sha.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -3825,19 +3809,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash-base": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
|
||||
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -5916,16 +5887,6 @@
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
|
||||
"dependencies": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -6591,19 +6552,6 @@
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -6694,15 +6642,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ripemd160": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
|
||||
"dependencies": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -6824,18 +6763,6 @@
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"sha.js": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -6988,14 +6915,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||
@@ -7397,11 +7316,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -7470,14 +7384,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wif": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
|
||||
"integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==",
|
||||
"dependencies": {
|
||||
"bs58check": "<3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
@@ -8725,6 +8631,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@mempool/electrum-client/-/electrum-client-1.1.9.tgz",
|
||||
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
|
||||
"integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -9445,12 +9356,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"base-x": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
|
||||
"integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
|
||||
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
|
||||
},
|
||||
"bech32": {
|
||||
"version": "2.0.0",
|
||||
@@ -9463,18 +9371,16 @@
|
||||
"integrity": "sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA=="
|
||||
},
|
||||
"bitcoinjs-lib": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.0.tgz",
|
||||
"integrity": "sha512-eupi1FBTJmPuAZdChnzTXLv2HBqFW2AICpzXZQLniP0V9FWWeeUQSMKES6sP8isy/xO0ijDexbgkdEyFVrsuJw==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
|
||||
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
|
||||
"requires": {
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bech32": "^2.0.0",
|
||||
"bip174": "^2.1.0",
|
||||
"bs58check": "^2.1.2",
|
||||
"create-hash": "^1.1.0",
|
||||
"ripemd160": "^2.0.2",
|
||||
"bs58check": "^3.0.1",
|
||||
"typeforce": "^1.11.3",
|
||||
"varuint-bitcoin": "^1.1.2",
|
||||
"wif": "^2.0.1"
|
||||
"varuint-bitcoin": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"body-parser": {
|
||||
@@ -9552,21 +9458,20 @@
|
||||
}
|
||||
},
|
||||
"bs58": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"requires": {
|
||||
"base-x": "^3.0.2"
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"bs58check": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
|
||||
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
|
||||
"integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
|
||||
"requires": {
|
||||
"bs58": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"safe-buffer": "^5.1.2"
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"bs58": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"bser": {
|
||||
@@ -9639,15 +9544,6 @@
|
||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"cjs-module-lexer": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
|
||||
@@ -9735,18 +9631,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"inherits": "^2.0.1",
|
||||
"md5.js": "^1.3.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"sha.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -10513,16 +10397,6 @@
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
|
||||
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -12069,16 +11943,6 @@
|
||||
"tiny-lru": "10.3.0"
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
|
||||
"requires": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -12547,16 +12411,6 @@
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -12618,15 +12472,6 @@
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"ripemd160": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
|
||||
"requires": {
|
||||
"hash-base": "^3.0.0",
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -12715,15 +12560,6 @@
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -12840,14 +12676,6 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||
@@ -13101,11 +12929,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -13159,14 +12982,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wif": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz",
|
||||
"integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==",
|
||||
"requires": {
|
||||
"bs58check": "<3.0.0"
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
|
||||
"start": "node --max-old-space-size=2048 dist/index.js",
|
||||
"start-production": "node --max-old-space-size=16384 dist/index.js",
|
||||
"reindex-updated-pools": "npm run start-production --update-pools",
|
||||
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
|
||||
"test": "./node_modules/.bin/jest --coverage",
|
||||
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"INITIAL_BLOCKS_AMOUNT": 7,
|
||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||
"USE_SECOND_NODE_FOR_MINFEE": 10,
|
||||
"EXTERNAL_ASSETS": 11,
|
||||
"EXTERNAL_ASSETS": [],
|
||||
"EXTERNAL_MAX_RETRY": 12,
|
||||
"EXTERNAL_RETRY_INTERVAL": 13,
|
||||
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
||||
@@ -24,19 +24,19 @@
|
||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
||||
"AUDIT": "__MEMPOOL_AUDIT__",
|
||||
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
|
||||
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
|
||||
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
|
||||
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
|
||||
"DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
|
||||
"AUDIT": true,
|
||||
"ADVANCED_GBT_AUDIT": true,
|
||||
"ADVANCED_GBT_MEMPOOL": true,
|
||||
"CPFP_INDEXING": true,
|
||||
"MAX_BLOCKS_BULK_QUERY": 999,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 999
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__CORE_RPC_HOST__",
|
||||
"PORT": 15,
|
||||
"USERNAME": "__CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": "__CORE_RPC_TIMEOUT__"
|
||||
"TIMEOUT": 1000
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
@@ -46,14 +46,14 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"RETRY_UNIX_SOCKET_AFTER": "__ESPLORA_RETRY_UNIX_SOCKET_AFTER__"
|
||||
"RETRY_UNIX_SOCKET_AFTER": 888
|
||||
},
|
||||
"SECOND_CORE_RPC": {
|
||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||
"PORT": 17,
|
||||
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
|
||||
"TIMEOUT": 2000
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": false,
|
||||
@@ -63,7 +63,7 @@
|
||||
"DATABASE": "__DATABASE_DATABASE__",
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": "__DATABASE_TIMEOUT__"
|
||||
"TIMEOUT": 3000
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": false,
|
||||
@@ -101,14 +101,14 @@
|
||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": "__LIGHTNING_ENABLED__",
|
||||
"ENABLED": true,
|
||||
"BACKEND": "__LIGHTNING_BACKEND__",
|
||||
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
|
||||
"STATS_REFRESH_INTERVAL": 600,
|
||||
"GRAPH_REFRESH_INTERVAL": 600,
|
||||
"LOGGER_UPDATE_INTERVAL": 30,
|
||||
"FORENSICS_INTERVAL": 43200,
|
||||
"FORENSICS_RATE_LIMIT": "__FORENSICS_RATE_LIMIT__"
|
||||
"FORENSICS_RATE_LIMIT": 1234
|
||||
},
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "",
|
||||
@@ -118,5 +118,9 @@
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
},
|
||||
"MEMPOOl_SERVICES": {
|
||||
"API": "__MEMPOOL_SERVICES_API__",
|
||||
"ACCELERATIONS": "__MEMPOOL_SERVICES_ACCELERATIONS__"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
750134, // Current block height
|
||||
0.6280047707459726, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
0, // If not testnet, not used
|
||||
0, // Latest block timestamp in seconds (only used if difficulty already locked in)
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
@@ -41,7 +41,7 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
],
|
||||
{ // Expected Result is same other than timeOffset
|
||||
progressPercent: 9.027777777777777,
|
||||
difficultyChange: 12.562233927411782,
|
||||
difficultyChange: 13.180707740199772,
|
||||
estimatedRetargetDate: 1661895424692,
|
||||
remainingBlocks: 1834,
|
||||
remainingTime: 977591692,
|
||||
@@ -54,6 +54,29 @@ describe('Mempool Difficulty Adjustment', () => {
|
||||
expectedBlocks: 161.68833333333333,
|
||||
},
|
||||
],
|
||||
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
|
||||
[ // Inputs
|
||||
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
|
||||
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
|
||||
788255, // Current block height
|
||||
1.7220298879531821, // Previous retarget % (Passed through)
|
||||
'mainnet', // Network (if testnet, next value is non-zero)
|
||||
dt('2023-05-04T14:54:26.000Z'), // Latest block timestamp in seconds
|
||||
],
|
||||
{ // Expected Result
|
||||
progressPercent: 99.95039682539682,
|
||||
difficultyChange: -1.4512637555574193,
|
||||
estimatedRetargetDate: 1683212658129,
|
||||
remainingBlocks: 1,
|
||||
remainingTime: 609129,
|
||||
previousRetarget: 1.7220298879531821,
|
||||
previousTime: 1681984653,
|
||||
nextRetargetHeight: 788256,
|
||||
timeAvg: 609129,
|
||||
timeOffset: 0,
|
||||
expectedBlocks: 2045.66,
|
||||
},
|
||||
],
|
||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||
|
||||
for (const vector of vectors) {
|
||||
|
||||
@@ -117,6 +117,11 @@ describe('Mempool Backend Config', () => {
|
||||
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||
});
|
||||
|
||||
expect(config.MEMPOOL_SERVICES).toStrictEqual({
|
||||
API: "",
|
||||
ACCELERATIONS: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,6 +155,98 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
|
||||
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
|
||||
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure the docker start.sh script has default values', () => {
|
||||
jest.isolateModules(() => {
|
||||
const startSh = fs.readFileSync(`${__dirname}/../../../docker/backend/start.sh`, 'utf-8');
|
||||
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
|
||||
|
||||
function parseJson(jsonObj, root?) {
|
||||
for (const [key, value] of Object.entries(jsonObj)) {
|
||||
// We have a few cases where we can't follow the pattern
|
||||
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||
return;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
if (Array.isArray(value)) {
|
||||
return;
|
||||
} else {
|
||||
parseJson(value, key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
//The flattened string, i.e, __MEMPOOL_ENABLED__
|
||||
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
|
||||
|
||||
//The string used as the environment variable, i.e, MEMPOOL_ENABLED
|
||||
const envVarStr = `${root ? root : ''}_${key}`;
|
||||
|
||||
//The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
|
||||
const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
|
||||
|
||||
console.log(`looking for ${defaultEntry} in the start.sh script`);
|
||||
const re = new RegExp(defaultEntry);
|
||||
expect(startSh).toMatch(re);
|
||||
|
||||
//The string that actually replaces the values in the config file
|
||||
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
|
||||
console.log(`looking for ${sedStr} in the start.sh script`);
|
||||
expect(startSh).toContain(sedStr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parseJson(fixture);
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure that the mempool-config.json Docker template has all the keys', () => {
|
||||
jest.isolateModules(() => {
|
||||
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
|
||||
const dockerJson = fs.readFileSync(`${__dirname}/../../../docker/backend/mempool-config.json`, 'utf-8');
|
||||
|
||||
function parseJson(jsonObj, root?) {
|
||||
for (const [key, value] of Object.entries(jsonObj)) {
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
if (Array.isArray(value)) {
|
||||
// numbers, arrays and booleans won't be enclosed by quotes
|
||||
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
|
||||
expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
|
||||
break;
|
||||
} else {
|
||||
//Check for top level config keys
|
||||
expect(dockerJson).toContain(`"${key}"`);
|
||||
parseJson(value, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case 'string': {
|
||||
// strings should be enclosed by quotes
|
||||
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
|
||||
expect(dockerJson).toContain(`"${key}": "${replaceStr}"`);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// numbers, arrays and booleans won't be enclosed by quotes
|
||||
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
|
||||
expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
parseJson(fixture);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -5,15 +5,16 @@ import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.in
|
||||
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 {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||
: { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
|
||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], accelerated: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
|
||||
return { censored: [], added: [], fresh: [], sigop: [], accelerated: [], score: 0, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
@@ -26,6 +27,9 @@ class Audit {
|
||||
const now = Math.round((Date.now() / 1000));
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = tx;
|
||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||
accelerated.push(tx.txid);
|
||||
}
|
||||
}
|
||||
// coinbase is always expected
|
||||
if (transactions[0]) {
|
||||
@@ -137,6 +141,8 @@ class Audit {
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
fresh,
|
||||
sigop: [],
|
||||
accelerated,
|
||||
score,
|
||||
similarity,
|
||||
};
|
||||
|
||||
@@ -415,12 +415,38 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 2];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
||||
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
||||
if (witnessScript !== null) {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function must only be called when we know the witness we are parsing
|
||||
* is a taproot witness.
|
||||
* @param witness An array of hex strings that represents the witness stack of
|
||||
* the input.
|
||||
* @returns null if the witness is not a script spend, and the hex string of
|
||||
* the script item if it is a script spend.
|
||||
*/
|
||||
private witnessToP2TRScript(witness: string[]): string | null {
|
||||
if (witness.length < 2) return null;
|
||||
// Note: see BIP341 for parsing details of witness stack
|
||||
|
||||
// If there are at least two witness elements, and the first byte of the
|
||||
// last element is 0x50, this last element is called annex a and
|
||||
// is removed from the witness stack.
|
||||
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
||||
// If there are at least two witness elements left, script path spending is used.
|
||||
// Call the second-to-last stack element s, the script.
|
||||
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
||||
if (hasAnnex && witness.length < 3) return null;
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
|
||||
@@ -130,8 +130,9 @@ class BitcoinRoutes {
|
||||
|
||||
private getInitData(req: Request, res: Response) {
|
||||
try {
|
||||
const result = websocketHandler.getInitData();
|
||||
res.json(result);
|
||||
const result = websocketHandler.getSerializedInitData();
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
@@ -210,6 +211,9 @@ class BitcoinRoutes {
|
||||
bestDescendant: tx.bestDescendant || null,
|
||||
descendants: tx.descendants || null,
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||
sigops: tx.sigops,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import config from '../config';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import logger from '../logger';
|
||||
import memPool from './mempool';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary } from '../mempool.interfaces';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
@@ -76,6 +76,7 @@ class Blocks {
|
||||
blockHeight: number,
|
||||
onlyCoinbase: boolean,
|
||||
quiet: boolean = false,
|
||||
addMempoolData: boolean = false,
|
||||
): Promise<TransactionExtended[]> {
|
||||
const transactions: TransactionExtended[] = [];
|
||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||
@@ -96,14 +97,14 @@ class Blocks {
|
||||
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||
}
|
||||
try {
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
|
||||
transactions.push(tx);
|
||||
transactionsFetched++;
|
||||
} catch (e) {
|
||||
try {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
// Try again with core
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true);
|
||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
|
||||
transactions.push(tx);
|
||||
transactionsFetched++;
|
||||
} else {
|
||||
@@ -126,11 +127,13 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (!tx.cpfpChecked) {
|
||||
Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent
|
||||
}
|
||||
});
|
||||
if (addMempoolData) {
|
||||
transactions.forEach((tx) => {
|
||||
if (!tx.cpfpChecked) {
|
||||
Common.setRelativesAndGetCpfpInfo(tx as MempoolTransactionExtended, mempool); // Child Pay For Parent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
|
||||
@@ -306,7 +309,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
|
||||
const address = txMinerInfo.vout[0].scriptpubkey_address;
|
||||
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
|
||||
|
||||
let pools: PoolTag[] = [];
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
@@ -316,11 +319,13 @@ class Blocks {
|
||||
}
|
||||
|
||||
for (let i = 0; i < pools.length; ++i) {
|
||||
if (address !== undefined) {
|
||||
const addresses: string[] = typeof pools[i].addresses === 'string' ?
|
||||
if (addresses.length) {
|
||||
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
|
||||
JSON.parse(pools[i].addresses) : pools[i].addresses;
|
||||
if (addresses.indexOf(address) !== -1) {
|
||||
return pools[i];
|
||||
for (let y = 0; y < poolAddresses.length; y++) {
|
||||
if (addresses.indexOf(poolAddresses[y]) !== -1) {
|
||||
return pools[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +599,15 @@ class Blocks {
|
||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, false, true);
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||
import config from '../config';
|
||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||
import { isIP } from 'net';
|
||||
@@ -57,15 +57,15 @@ export class Common {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } {
|
||||
const matches: { [txid: string]: TransactionExtended[] } = {};
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
|
||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
||||
added
|
||||
.forEach((addedTx) => {
|
||||
const foundMatches = deleted.filter((deletedTx) => {
|
||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||
return addedTx.fee > deletedTx.fee
|
||||
// The new transaction must pay more fee per kB than the replaced tx.
|
||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
||||
&& addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
|
||||
// Spends one or more of the same inputs
|
||||
&& deletedTx.vin.some((deletedVin) =>
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||
@@ -77,12 +77,31 @@ export class Common {
|
||||
return matches;
|
||||
}
|
||||
|
||||
static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, MempoolTransactionExtended>): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||
for (const tx of minedTransactions) {
|
||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||
for (let i = 0; i < tx.vin.length; i++) {
|
||||
const vin = tx.vin[i];
|
||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||
if (match && match.txid !== tx.txid) {
|
||||
replaced.add(match);
|
||||
}
|
||||
}
|
||||
if (replaced.size) {
|
||||
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
fee: tx.fee,
|
||||
vsize: tx.weight / 4,
|
||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||
acc: tx.acceleration || undefined,
|
||||
rate: tx.effectiveFeePerVsize,
|
||||
};
|
||||
}
|
||||
@@ -102,18 +121,18 @@ export class Common {
|
||||
}
|
||||
}
|
||||
|
||||
static setRelativesAndGetCpfpInfo(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): CpfpInfo {
|
||||
static setRelativesAndGetCpfpInfo(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
const parents = this.findAllParents(tx, memPool);
|
||||
const lowerFeeParents = parents.filter((parent) => parent.feePerVsize < tx.effectiveFeePerVsize);
|
||||
const lowerFeeParents = parents.filter((parent) => parent.adjustedFeePerVsize < tx.effectiveFeePerVsize);
|
||||
|
||||
let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
||||
let totalWeight = (tx.adjustedVsize * 4) + lowerFeeParents.reduce((prev, val) => prev + (val.adjustedVsize * 4), 0);
|
||||
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||
|
||||
tx.ancestors = parents
|
||||
.map((t) => {
|
||||
return {
|
||||
txid: t.txid,
|
||||
weight: t.weight,
|
||||
weight: (t.adjustedVsize * 4),
|
||||
fee: t.fee,
|
||||
};
|
||||
});
|
||||
@@ -134,8 +153,8 @@ export class Common {
|
||||
}
|
||||
|
||||
|
||||
private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] {
|
||||
let parents: TransactionExtended[] = [];
|
||||
private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
|
||||
let parents: MempoolTransactionExtended[] = [];
|
||||
tx.vin.forEach((parent) => {
|
||||
if (parents.find((p) => p.txid === parent.txid)) {
|
||||
return;
|
||||
@@ -143,17 +162,17 @@ export class Common {
|
||||
|
||||
const parentTx = memPool[parent.txid];
|
||||
if (parentTx) {
|
||||
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.feePerVsize) {
|
||||
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.adjustedFeePerVsize) {
|
||||
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
|
||||
parentTx.bestDescendant = {
|
||||
weight: tx.weight + tx.bestDescendant.weight,
|
||||
weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
|
||||
fee: tx.fee + tx.bestDescendant.fee,
|
||||
txid: tx.txid,
|
||||
};
|
||||
}
|
||||
} else if (tx.feePerVsize > parentTx.feePerVsize) {
|
||||
} else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
|
||||
parentTx.bestDescendant = {
|
||||
weight: tx.weight,
|
||||
weight: (tx.adjustedVsize * 4),
|
||||
fee: tx.fee,
|
||||
txid: tx.txid
|
||||
};
|
||||
@@ -442,3 +461,119 @@ export class Common {
|
||||
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to calculate average fee rates of a list of transactions
|
||||
* at certain weight percentiles, in a single pass
|
||||
*
|
||||
* init with:
|
||||
* maxWeight - the total weight to measure percentiles relative to (e.g. 4MW for a single block)
|
||||
* percentileBandWidth - how many weight units to average over for each percentile (as a % of maxWeight)
|
||||
* percentiles - an array of weight percentiles to compute, in %
|
||||
*
|
||||
* then call .processNext(tx) for each transaction, in descending order
|
||||
*
|
||||
* retrieve the final results with .getFeeStats()
|
||||
*/
|
||||
export class OnlineFeeStatsCalculator {
|
||||
private maxWeight: number;
|
||||
private percentiles = [10,25,50,75,90];
|
||||
|
||||
private bandWidthPercent = 2;
|
||||
private bandWidth: number = 0;
|
||||
private bandIndex = 0;
|
||||
private leftBound = 0;
|
||||
private rightBound = 0;
|
||||
private inBand = false;
|
||||
private totalBandFee = 0;
|
||||
private totalBandWeight = 0;
|
||||
private minBandRate = Infinity;
|
||||
private maxBandRate = 0;
|
||||
|
||||
private feeRange: { avg: number, min: number, max: number }[] = [];
|
||||
private totalWeight: number = 0;
|
||||
|
||||
constructor (maxWeight: number, percentileBandWidth?: number, percentiles?: number[]) {
|
||||
this.maxWeight = maxWeight;
|
||||
if (percentiles && percentiles.length) {
|
||||
this.percentiles = percentiles;
|
||||
}
|
||||
if (percentileBandWidth != null) {
|
||||
this.bandWidthPercent = percentileBandWidth;
|
||||
}
|
||||
this.bandWidth = this.maxWeight * (this.bandWidthPercent / 100);
|
||||
// add min/max percentiles aligned to the ends of the range
|
||||
this.percentiles.unshift(this.bandWidthPercent / 2);
|
||||
this.percentiles.push(100 - (this.bandWidthPercent / 2));
|
||||
this.setNextBounds();
|
||||
}
|
||||
|
||||
processNext(tx: { weight: number, fee: number, effectiveFeePerVsize?: number, feePerVsize?: number, rate?: number, txid: string }): void {
|
||||
let left = this.totalWeight;
|
||||
const right = this.totalWeight + tx.weight;
|
||||
if (!this.inBand && right <= this.leftBound) {
|
||||
this.totalWeight += tx.weight;
|
||||
return;
|
||||
}
|
||||
|
||||
while (left < right) {
|
||||
if (right > this.leftBound) {
|
||||
this.inBand = true;
|
||||
const txRate = (tx.rate || tx.effectiveFeePerVsize || tx.feePerVsize || 0);
|
||||
const weight = Math.min(right, this.rightBound) - Math.max(left, this.leftBound);
|
||||
this.totalBandFee += (txRate * weight);
|
||||
this.totalBandWeight += weight;
|
||||
this.maxBandRate = Math.max(this.maxBandRate, txRate);
|
||||
this.minBandRate = Math.min(this.minBandRate, txRate);
|
||||
}
|
||||
left = Math.min(right, this.rightBound);
|
||||
|
||||
if (left >= this.rightBound) {
|
||||
this.inBand = false;
|
||||
const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
|
||||
this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
|
||||
this.bandIndex++;
|
||||
this.setNextBounds();
|
||||
this.totalBandFee = 0;
|
||||
this.totalBandWeight = 0;
|
||||
this.minBandRate = Infinity;
|
||||
this.maxBandRate = 0;
|
||||
}
|
||||
}
|
||||
this.totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
private setNextBounds(): void {
|
||||
const nextPercentile = this.percentiles[this.bandIndex];
|
||||
if (nextPercentile != null) {
|
||||
this.leftBound = ((nextPercentile / 100) * this.maxWeight) - (this.bandWidth / 2);
|
||||
this.rightBound = this.leftBound + this.bandWidth;
|
||||
} else {
|
||||
this.leftBound = Infinity;
|
||||
this.rightBound = Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
getRawFeeStats(): WorkingEffectiveFeeStats {
|
||||
if (this.totalBandWeight > 0) {
|
||||
const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
|
||||
this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
|
||||
}
|
||||
while (this.feeRange.length < this.percentiles.length) {
|
||||
this.feeRange.unshift({ avg: 0, min: 0, max: 0 });
|
||||
}
|
||||
return {
|
||||
minFee: this.feeRange[0].min,
|
||||
medianFee: this.feeRange[Math.floor(this.feeRange.length / 2)].avg,
|
||||
maxFee: this.feeRange[this.feeRange.length - 1].max,
|
||||
feeRange: this.feeRange.map(f => f.avg),
|
||||
};
|
||||
}
|
||||
|
||||
getFeeStats(): EffectiveFeeStats {
|
||||
const stats = this.getRawFeeStats();
|
||||
stats.feeRange[0] = stats.minFee;
|
||||
stats.feeRange[stats.feeRange.length - 1] = stats.maxFee;
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 59;
|
||||
private static currentVersion = 62;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -516,6 +516,27 @@ class DatabaseMigration {
|
||||
// https://github.com/mempool/mempool/issues/3360
|
||||
await this.$executeQuery(`TRUNCATE prices`);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 60 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(60);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 61 && isBitcoin === true) {
|
||||
// Break block templates into their own table
|
||||
if (! await this.$checkIfTableExists('blocks_templates')) {
|
||||
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
|
||||
}
|
||||
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
|
||||
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
|
||||
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
|
||||
await this.updateToSchemaVersion(61);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 62 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(61);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,11 +34,12 @@ export function calcDifficultyAdjustment(
|
||||
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
||||
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
||||
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
|
||||
const actualTimespan = (blocksInEpoch === 2015 ? latestBlockTimestamp : nowSeconds) - DATime;
|
||||
|
||||
let difficultyChange = 0;
|
||||
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
|
||||
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
||||
difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
|
||||
// Max increase is x4 (+300%)
|
||||
if (difficultyChange > 300) {
|
||||
difficultyChange = 300;
|
||||
|
||||
@@ -21,6 +21,7 @@ class DiskCache {
|
||||
private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json';
|
||||
private static CHUNK_FILES = 25;
|
||||
private isWritingCache = false;
|
||||
private ignoreBlocksCache = false;
|
||||
|
||||
private semaphore: { resume: (() => void)[], locks: number } = {
|
||||
resume: [],
|
||||
@@ -218,8 +219,13 @@ class DiskCache {
|
||||
}
|
||||
|
||||
await memPool.$setMempool(data.mempool);
|
||||
blocks.setBlocks(data.blocks);
|
||||
blocks.setBlockSummaries(data.blockSummaries || []);
|
||||
if (!this.ignoreBlocksCache) {
|
||||
blocks.setBlocks(data.blocks);
|
||||
blocks.setBlockSummaries(data.blockSummaries || []);
|
||||
} else {
|
||||
logger.info('Re-saving cache with empty recent blocks data');
|
||||
await this.$saveCacheToDisk(true);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -273,6 +279,10 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setIgnoreBlocksCache(): void {
|
||||
this.ignoreBlocksCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new DiskCache();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import logger from '../logger';
|
||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
|
||||
import { Common, OnlineFeeStatsCalculator } from './common';
|
||||
import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import mempool from './mempool';
|
||||
|
||||
class MempoolBlocks {
|
||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
@@ -36,9 +37,9 @@ class MempoolBlocks {
|
||||
return this.mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
||||
public updateMempoolBlocks(memPool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
||||
const latestMempool = memPool;
|
||||
const memPoolArray: TransactionExtended[] = [];
|
||||
const memPoolArray: MempoolTransactionExtended[] = [];
|
||||
for (const i in latestMempool) {
|
||||
if (latestMempool.hasOwnProperty(i)) {
|
||||
memPoolArray.push(latestMempool[i]);
|
||||
@@ -52,17 +53,17 @@ class MempoolBlocks {
|
||||
tx.ancestors = [];
|
||||
tx.cpfpChecked = false;
|
||||
if (!tx.effectiveFeePerVsize) {
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
// First sort
|
||||
memPoolArray.sort((a, b) => {
|
||||
if (a.feePerVsize === b.feePerVsize) {
|
||||
if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
|
||||
// tie-break by lexicographic txid order for stability
|
||||
return a.txid < b.txid ? -1 : 1;
|
||||
} else {
|
||||
return b.feePerVsize - a.feePerVsize;
|
||||
return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,16 +103,18 @@ class MempoolBlocks {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
|
||||
let onlineStats = false;
|
||||
let blockSize = 0;
|
||||
let blockWeight = 0;
|
||||
let blockVsize = 0;
|
||||
let blockFees = 0;
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
let transactionIds: string[] = [];
|
||||
let transactions: TransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx) => {
|
||||
let transactions: MempoolTransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx, index) => {
|
||||
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|
||||
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
tx.position = {
|
||||
@@ -126,6 +129,9 @@ class MempoolBlocks {
|
||||
transactions.push(tx);
|
||||
}
|
||||
transactionIds.push(tx.txid);
|
||||
if (onlineStats) {
|
||||
feeStatsCalculator.processNext(tx);
|
||||
}
|
||||
} else {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
|
||||
blockVsize = 0;
|
||||
@@ -133,6 +139,16 @@ class MempoolBlocks {
|
||||
block: mempoolBlocks.length,
|
||||
vsize: blockVsize + (tx.vsize / 2),
|
||||
};
|
||||
|
||||
if (mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
|
||||
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
|
||||
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||
onlineStats = true;
|
||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
|
||||
feeStatsCalculator.processNext(tx);
|
||||
}
|
||||
}
|
||||
|
||||
blockVsize += tx.vsize;
|
||||
blockWeight = tx.weight;
|
||||
blockSize = tx.size;
|
||||
@@ -142,7 +158,8 @@ class MempoolBlocks {
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
|
||||
const feeStats = onlineStats ? feeStatsCalculator.getRawFeeStats() : undefined;
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees, feeStats));
|
||||
}
|
||||
|
||||
return mempoolBlocks;
|
||||
@@ -153,7 +170,7 @@ class MempoolBlocks {
|
||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||
let added: TransactionStripped[] = [];
|
||||
let removed: string[] = [];
|
||||
const changed: { txid: string, rate: number | undefined }[] = [];
|
||||
const changed: { txid: string, rate: number | undefined, acc: number | undefined }[] = [];
|
||||
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
||||
added = mempoolBlocks[i].transactions;
|
||||
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
||||
@@ -175,8 +192,8 @@ class MempoolBlocks {
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
if (!prevIds[tx.txid]) {
|
||||
added.push(tx);
|
||||
} else if (tx.rate !== prevIds[tx.txid].rate) {
|
||||
changed.push({ txid: tx.txid, rate: tx.rate });
|
||||
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
|
||||
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -189,15 +206,17 @@ class MempoolBlocks {
|
||||
return mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||
const start = Date.now();
|
||||
|
||||
// reset mempool short ids
|
||||
this.resetUids();
|
||||
for (const tx of Object.values(newMempool)) {
|
||||
this.setUid(tx);
|
||||
this.setUid(tx, true);
|
||||
}
|
||||
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
|
||||
@@ -205,10 +224,11 @@ class MempoolBlocks {
|
||||
if (entry.uid != null) {
|
||||
strippedMempool.set(entry.uid, {
|
||||
uid: entry.uid,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
|
||||
weight: (entry.adjustedVsize * 4),
|
||||
sigops: entry.sigops,
|
||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
});
|
||||
}
|
||||
@@ -243,7 +263,7 @@ class MempoolBlocks {
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
|
||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||
return processed;
|
||||
} catch (e) {
|
||||
@@ -252,28 +272,33 @@ class MempoolBlocks {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
|
||||
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
|
||||
if (!this.txSelectionWorker) {
|
||||
// need to reset the worker
|
||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
||||
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
for (const tx of Object.values(added)) {
|
||||
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
|
||||
|
||||
for (const tx of addedAndChanged) {
|
||||
this.setUid(tx);
|
||||
}
|
||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||
|
||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||
// to reduce the overhead of passing this data to the worker thread
|
||||
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
|
||||
const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
|
||||
return {
|
||||
uid: entry.uid || 0,
|
||||
fee: entry.fee,
|
||||
weight: entry.weight,
|
||||
feePerVsize: entry.fee / (entry.weight / 4),
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)),
|
||||
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
|
||||
weight: (entry.adjustedVsize * 4),
|
||||
sigops: entry.sigops,
|
||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
|
||||
};
|
||||
});
|
||||
@@ -296,32 +321,41 @@ class MempoolBlocks {
|
||||
// clean up thread error listener
|
||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults);
|
||||
this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
|
||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||
} catch (e) {
|
||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] {
|
||||
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, accelerations, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const txid of Object.keys(rates)) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].effectiveFeePerVsize = rates[txid];
|
||||
}
|
||||
}
|
||||
|
||||
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
|
||||
let hasBlockStack = blocks.length >= 8;
|
||||
let stackWeight;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
||||
if (hasBlockStack) {
|
||||
stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
|
||||
hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
|
||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
|
||||
}
|
||||
|
||||
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = [];
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block: string[] = blocks[blockIndex];
|
||||
let txid: string;
|
||||
let mempoolTx: TransactionExtended;
|
||||
let mempoolTx: MempoolTransactionExtended;
|
||||
let totalSize = 0;
|
||||
let totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
let totalFees = 0;
|
||||
const transactions: TransactionExtended[] = [];
|
||||
const transactions: MempoolTransactionExtended[] = [];
|
||||
for (let txIndex = 0; txIndex < block.length; txIndex++) {
|
||||
txid = block[txIndex];
|
||||
if (txid) {
|
||||
@@ -333,6 +367,13 @@ class MempoolBlocks {
|
||||
};
|
||||
mempoolTx.cpfpChecked = true;
|
||||
|
||||
mempoolTx.acceleration = accelerations[txid];
|
||||
|
||||
// online calculation of stack-of-blocks fee stats
|
||||
if (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) {
|
||||
feeStatsCalculator.processNext(mempoolTx);
|
||||
}
|
||||
|
||||
totalSize += mempoolTx.size;
|
||||
totalVsize += mempoolTx.vsize;
|
||||
totalWeight += mempoolTx.weight;
|
||||
@@ -348,7 +389,8 @@ class MempoolBlocks {
|
||||
transactions,
|
||||
totalSize,
|
||||
totalWeight,
|
||||
totalFees
|
||||
totalFees,
|
||||
feeStats: (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,7 +408,7 @@ class MempoolBlocks {
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: mempool[txid].weight,
|
||||
weight: (mempool[txid].adjustedVsize * 4),
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
@@ -382,7 +424,9 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees));
|
||||
const mempoolBlocks = readyBlocks.map((b, index) => {
|
||||
return this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees, b.feeStats);
|
||||
});
|
||||
|
||||
if (saveResults) {
|
||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||
@@ -393,8 +437,10 @@ class MempoolBlocks {
|
||||
return mempoolBlocks;
|
||||
}
|
||||
|
||||
private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions {
|
||||
const feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
||||
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
||||
if (!feeStats) {
|
||||
feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
||||
}
|
||||
return {
|
||||
blockSize: totalSize,
|
||||
blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
|
||||
@@ -412,15 +458,21 @@ class MempoolBlocks {
|
||||
this.nextUid = 1;
|
||||
}
|
||||
|
||||
private setUid(tx: TransactionExtended): number {
|
||||
const uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
// use reset=true to overwrite existing uids held by tx objects (required after resetUids)
|
||||
private setUid(tx: MempoolTransactionExtended, reset = false): number {
|
||||
let uid = reset ? null : this.getUid(tx);
|
||||
if (uid == null) {
|
||||
uid = this.nextUid;
|
||||
this.nextUid++;
|
||||
this.uidMap.set(uid, tx.txid);
|
||||
tx.uid = uid;
|
||||
return uid;
|
||||
} else {
|
||||
return uid;
|
||||
}
|
||||
}
|
||||
|
||||
private getUid(tx: TransactionExtended): number | void {
|
||||
private getUid(tx: MempoolTransactionExtended): number | void {
|
||||
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
|
||||
return tx.uid;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import config from '../config';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
import transactionUtils from './transaction-utils';
|
||||
@@ -9,17 +9,21 @@ import loadingIndicators from './loading-indicators';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import accelerationApi, { Acceleration } from './services/acceleration';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
private mempoolCacheDelta: number = -1;
|
||||
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: number } = {};
|
||||
|
||||
private txPerSecondArray: number[] = [];
|
||||
private txPerSecond: number = 0;
|
||||
@@ -63,28 +67,38 @@ class Mempool {
|
||||
return this.latestTransactions;
|
||||
}
|
||||
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
|
||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
||||
public getMempool(): { [txid: string]: MempoolTransactionExtended } {
|
||||
return this.mempoolCache;
|
||||
}
|
||||
|
||||
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
public getSpendMap(): Map<string, MempoolTransactionExtended> {
|
||||
return this.spendMap;
|
||||
}
|
||||
|
||||
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
for (const txid of Object.keys(this.mempoolCache)) {
|
||||
if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
}
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback) {
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||
}
|
||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
||||
}
|
||||
|
||||
public async $updateMemPoolInfo() {
|
||||
@@ -127,7 +141,7 @@ class Mempool {
|
||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||
this.updateTimerProgress(timer, 'got raw mempool');
|
||||
const diff = transactions.length - currentMempoolSize;
|
||||
const newTransactions: TransactionExtended[] = [];
|
||||
const newTransactions: MempoolTransactionExtended[] = [];
|
||||
|
||||
this.mempoolCacheDelta = Math.abs(diff);
|
||||
|
||||
@@ -149,7 +163,7 @@ class Mempool {
|
||||
for (const txid of transactions) {
|
||||
if (!this.mempoolCache[txid]) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
||||
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
|
||||
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||
this.mempoolCache[txid] = transaction;
|
||||
if (this.inSync) {
|
||||
@@ -199,7 +213,7 @@ class Mempool {
|
||||
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
||||
}
|
||||
|
||||
const deletedTransactions: TransactionExtended[] = [];
|
||||
const deletedTransactions: MempoolTransactionExtended[] = [];
|
||||
|
||||
if (this.mempoolProtection !== 1) {
|
||||
this.mempoolProtection = 0;
|
||||
@@ -218,6 +232,11 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
const accelerationDelta = await this.$updateAccelerations();
|
||||
if (accelerationDelta.length) {
|
||||
hasChange = true;
|
||||
}
|
||||
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
@@ -230,11 +249,11 @@ class Mempool {
|
||||
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
}
|
||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||
}
|
||||
|
||||
@@ -245,6 +264,48 @@ class Mempool {
|
||||
this.clearTimer(timer);
|
||||
}
|
||||
|
||||
public getAccelerations(): { [txid: string]: number } {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<string[]> {
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||
|
||||
const changed: string[] = [];
|
||||
|
||||
const newAccelerationMap: { [txid: string]: number } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
newAccelerationMap[acceleration.txid] = acceleration.feeDelta;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else if (this.accelerations[acceleration.txid] !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
this.accelerations = newAccelerationMap;
|
||||
|
||||
return changed;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
const state: any = {
|
||||
start: Date.now(),
|
||||
@@ -267,7 +328,7 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
||||
// Store replaced transactions
|
||||
@@ -276,6 +337,34 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(rbfTransactions[rbfTransaction].replaced, transactionUtils.extendMempoolTransaction(rbfTransactions[rbfTransaction].replacedBy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addToSpendMap(transactions: MempoolTransactionExtended[]): void {
|
||||
for (const tx of transactions) {
|
||||
for (const vin of tx.vin) {
|
||||
this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromSpendMap(transactions: TransactionExtended[]): void {
|
||||
for (const tx of transactions) {
|
||||
for (const vin of tx.vin) {
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
if (this.spendMap.get(key)?.txid === tx.txid) {
|
||||
this.spendMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTxPerSecond() {
|
||||
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
|
||||
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
|
||||
|
||||
@@ -26,7 +26,7 @@ class MiningRoutes {
|
||||
.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/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
|
||||
@@ -244,15 +244,15 @@ class MiningRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getHistoricalBlockPrediction(req: Request, res: Response) {
|
||||
private async $getHistoricalBlocksHealth(req: Request, res: Response) {
|
||||
try {
|
||||
const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval);
|
||||
const blockCount = await BlocksAuditsRepository.$getPredictionsCount();
|
||||
const blocksHealth = await mining.$getBlocksHealthHistory(req.params.interval);
|
||||
const blockCount = await BlocksAuditsRepository.$getBlocksHealthCount();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.header('X-total-count', blockCount.toString());
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate]));
|
||||
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
class Mining {
|
||||
private blocksPriceIndexingRunning = false;
|
||||
@@ -21,10 +20,10 @@ class Mining {
|
||||
public lastWeeklyHashrateIndexingDate: number | null = null;
|
||||
|
||||
/**
|
||||
* Get historical block predictions match rate
|
||||
* Get historical blocks health
|
||||
*/
|
||||
public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> {
|
||||
return await BlocksAuditsRepository.$getBlockPredictionsHistory(
|
||||
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
|
||||
return await BlocksAuditsRepository.$getBlocksHealthHistory(
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
);
|
||||
|
||||
@@ -41,7 +41,7 @@ class PoolsParser {
|
||||
public async migratePoolsJson(): Promise<void> {
|
||||
// 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)
|
||||
diskCache.wipeCache();
|
||||
diskCache.setIgnoreBlocksCache();
|
||||
|
||||
await this.$insertUnknownPool();
|
||||
|
||||
@@ -118,10 +118,6 @@ class PoolsParser {
|
||||
* @param pool
|
||||
*/
|
||||
private async $deleteBlocksForPool(pool: PoolTag): Promise<void> {
|
||||
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
|
||||
// Ignore early days of Bitcoin as there were no mining pool yet
|
||||
const [oldestPoolBlock]: any[] = await DB.query(`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger from "../logger";
|
||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { Common } from "./common";
|
||||
|
||||
@@ -23,20 +23,20 @@ class RbfCache {
|
||||
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
|
||||
private dirtyTrees: Set<string> = new Set();
|
||||
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||
private txs: Map<string, TransactionExtended> = new Map();
|
||||
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
||||
private expiring: Map<string, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length) {
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||
const newTime = newTxExtended.firstSeen || Date.now();
|
||||
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
this.txs.set(newTx.txid, newTxExtended);
|
||||
|
||||
@@ -59,7 +59,7 @@ class RbfCache {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const replacedTime = replacedTxExtended.firstSeen || Date.now();
|
||||
const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000);
|
||||
replacedTrees.push({
|
||||
tx: replacedTx,
|
||||
time: replacedTime,
|
||||
@@ -74,7 +74,7 @@ class RbfCache {
|
||||
const treeId = replacedTrees[0].tx.txid;
|
||||
const newTree = {
|
||||
tx: newTx,
|
||||
time: newTxExtended.firstSeen || Date.now(),
|
||||
time: newTime,
|
||||
fullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
@@ -92,7 +92,7 @@ class RbfCache {
|
||||
return this.replaces.get(txId);
|
||||
}
|
||||
|
||||
public getTx(txId: string): TransactionExtended | undefined {
|
||||
public getTx(txId: string): MempoolTransactionExtended | undefined {
|
||||
return this.txs.get(txId);
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ class RbfCache {
|
||||
return deflated;
|
||||
}
|
||||
|
||||
async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
||||
async importTree(root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
||||
const treeInfo = deflated[txid];
|
||||
const replaces: RbfTree[] = [];
|
||||
|
||||
|
||||
38
backend/src/api/services/acceleration.ts
Normal file
38
backend/src/api/services/acceleration.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { query } from '../../utils/axios-query';
|
||||
import config from '../../config';
|
||||
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string,
|
||||
feeDelta: number,
|
||||
}
|
||||
|
||||
class AccelerationApi {
|
||||
public async $fetchAccelerations(): Promise<Acceleration[]> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerations`);
|
||||
return (response as Acceleration[]) || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $fetchPools(): Promise<PoolTag[]> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
const response = await query(`${config.MEMPOOL_SERVICES.API}/partners`);
|
||||
return (response as PoolTag[]) || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $isAcceleratedBlock(block: BlockExtended): Promise<boolean> {
|
||||
const pools = await this.$fetchPools();
|
||||
if (block?.extras?.pool?.id == null) {
|
||||
return false;
|
||||
}
|
||||
return pools.reduce((match, tag) => match || tag.uniqueId === block.extras.pool.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationApi();
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||
import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
import { Common } from './common';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
|
||||
class TransactionUtils {
|
||||
constructor() { }
|
||||
@@ -22,19 +23,27 @@ class TransactionUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId
|
||||
* @param addPrevouts
|
||||
* @param lazyPrevouts
|
||||
* @param txId
|
||||
* @param addPrevouts
|
||||
* @param lazyPrevouts
|
||||
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
||||
*/
|
||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> {
|
||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||
let transaction: IEsploraApi.Transaction;
|
||||
if (forceCore === true) {
|
||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
||||
} else {
|
||||
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||
}
|
||||
return this.extendTransaction(transaction);
|
||||
if (addMempoolData || !transaction?.status?.confirmed) {
|
||||
return this.extendMempoolTransaction(transaction);
|
||||
} else {
|
||||
return this.extendTransaction(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
|
||||
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
|
||||
}
|
||||
|
||||
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
||||
@@ -50,8 +59,32 @@ class TransactionUtils {
|
||||
feePerVsize: feePerVbytes,
|
||||
effectiveFeePerVsize: feePerVbytes,
|
||||
}, transaction);
|
||||
if (!transaction.status.confirmed) {
|
||||
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
||||
if (!transaction?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
}
|
||||
return transactionExtended;
|
||||
}
|
||||
|
||||
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
||||
const vsize = Math.ceil(transaction.weight / 4);
|
||||
const fractionalVsize = (transaction.weight / 4);
|
||||
const sigops = this.countSigops(transaction);
|
||||
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
||||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||
(transaction.fee || 0) / fractionalVsize);
|
||||
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
|
||||
(transaction.fee || 0) / adjustedVsize);
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
adjustedVsize,
|
||||
sigops,
|
||||
feePerVsize: feePerVbytes,
|
||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||
});
|
||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
}
|
||||
return transactionExtended;
|
||||
}
|
||||
@@ -63,6 +96,64 @@ class TransactionUtils {
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
|
||||
let sigops = 0;
|
||||
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
|
||||
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
|
||||
|
||||
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
|
||||
if (isRawScript) {
|
||||
// in scriptPubKey or scriptSig, always worth 20
|
||||
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
||||
} else {
|
||||
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
||||
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
|
||||
for (const match of matches) {
|
||||
const n = parseInt(match[1]);
|
||||
if (Number.isInteger(n)) {
|
||||
sigops += n;
|
||||
} else {
|
||||
sigops += 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return witness ? sigops : (sigops * 4);
|
||||
}
|
||||
|
||||
public countSigops(transaction: IEsploraApi.Transaction): number {
|
||||
let sigops = 0;
|
||||
|
||||
for (const input of transaction.vin) {
|
||||
if (input.scriptsig_asm) {
|
||||
sigops += this.countScriptSigops(input.scriptsig_asm, true);
|
||||
}
|
||||
if (input.prevout) {
|
||||
switch (true) {
|
||||
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
|
||||
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
|
||||
sigops += 1;
|
||||
break;
|
||||
|
||||
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
|
||||
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
|
||||
if (input.witness?.length) {
|
||||
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const output of transaction.vout) {
|
||||
if (output.scriptpubkey_asm) {
|
||||
sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
|
||||
}
|
||||
}
|
||||
|
||||
return sigops;
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
||||
@@ -48,12 +48,14 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
weight: tx.weight,
|
||||
feePerVsize: tx.feePerVsize,
|
||||
effectiveFeePerVsize: tx.feePerVsize,
|
||||
sigops: tx.sigops,
|
||||
inputs: tx.inputs || [],
|
||||
relativesSet: false,
|
||||
ancestorMap: new Map<number, AuditTransaction>(),
|
||||
children: new Set<AuditTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorWeight: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
@@ -83,6 +85,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 4000;
|
||||
let blockSigops = 0;
|
||||
let transactions: AuditTransaction[] = [];
|
||||
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
|
||||
if (a.score === b.score) {
|
||||
@@ -118,7 +121,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
|
||||
if (blocks.length >= 7 || ((blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) && (blockSigops + nextTx.ancestorSigops <= 80000))) {
|
||||
const ancestors: AuditTransaction[] = 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];
|
||||
@@ -127,7 +130,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
|
||||
isCluster = true;
|
||||
}
|
||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / (nextTx.ancestorWeight / 4));
|
||||
const used: AuditTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
@@ -155,7 +158,7 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
updateDescendants(tx, auditPool, modified);
|
||||
updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,9 +240,11 @@ function setRelatives(
|
||||
};
|
||||
tx.ancestorFee = tx.fee || 0;
|
||||
tx.ancestorWeight = tx.weight || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += ancestor.fee;
|
||||
tx.ancestorWeight += ancestor.weight;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
|
||||
tx.relativesSet = true;
|
||||
@@ -251,6 +256,7 @@ function updateDescendants(
|
||||
rootTx: AuditTransaction,
|
||||
mempool: Map<number, AuditTransaction>,
|
||||
modified: PairingHeap<AuditTransaction>,
|
||||
clusterRate: number,
|
||||
): void {
|
||||
const descendantSet: Set<AuditTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
@@ -270,8 +276,10 @@ function updateDescendants(
|
||||
descendantTx.ancestorMap.delete(rootTx.uid);
|
||||
descendantTx.ancestorFee -= rootTx.fee;
|
||||
descendantTx.ancestorWeight -= rootTx.weight;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
tmpScore = descendantTx.score;
|
||||
descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modifiedNode) {
|
||||
descendantTx.modified = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logger from '../logger';
|
||||
import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, WebsocketResponse,
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
@@ -21,6 +21,7 @@ import Audit from './audit';
|
||||
import { deepClone } from '../utils/clone';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -30,6 +31,9 @@ class WebsocketHandler {
|
||||
private numConnected = 0;
|
||||
private numDisconnected = 0;
|
||||
|
||||
private initData: { [key: string]: string } = {};
|
||||
private serializedInitData: string = '{}';
|
||||
|
||||
constructor() { }
|
||||
|
||||
setWebsocketServer(wss: WebSocket.Server) {
|
||||
@@ -38,6 +42,41 @@ class WebsocketHandler {
|
||||
|
||||
setExtraInitProperties(property: string, value: any) {
|
||||
this.extraInitProperties[property] = value;
|
||||
this.setInitDataFields(this.extraInitProperties);
|
||||
}
|
||||
|
||||
private setInitDataFields(data: { [property: string]: any }): void {
|
||||
for (const property of Object.keys(data)) {
|
||||
if (data[property] != null) {
|
||||
this.initData[property] = JSON.stringify(data[property]);
|
||||
} else {
|
||||
delete this.initData[property];
|
||||
}
|
||||
}
|
||||
this.serializedInitData = '{'
|
||||
+ Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
|
||||
+ '}';
|
||||
}
|
||||
|
||||
private updateInitData(): void {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
this.setInitDataFields({
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
'conversions': priceUpdater.getLatestPrices(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
});
|
||||
}
|
||||
|
||||
public getSerializedInitData(): string {
|
||||
return this.serializedInitData;
|
||||
}
|
||||
|
||||
setupConnectionHandling() {
|
||||
@@ -84,7 +123,7 @@ class WebsocketHandler {
|
||||
} else {
|
||||
// tx.prevout is missing from transactions when in bitcoind mode
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -92,7 +131,7 @@ class WebsocketHandler {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
||||
response['tx'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||
@@ -106,6 +145,7 @@ class WebsocketHandler {
|
||||
response['txPosition'] = {
|
||||
txid: trackTxid,
|
||||
position: tx.position,
|
||||
accelerated: tx.acceleration || undefined,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -157,11 +197,13 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
if (!_blocks) {
|
||||
if (!this.initData['blocks']?.length || !this.initData['da']) {
|
||||
this.updateInitData();
|
||||
}
|
||||
if (!this.initData['blocks']?.length) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify(this.getInitData(_blocks)));
|
||||
client.send(this.serializedInitData);
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'ping') {
|
||||
@@ -210,11 +252,14 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.setInitDataFields({ 'loadingIndicators': indicators });
|
||||
|
||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ loadingIndicators: indicators }));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,34 +268,17 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.setInitDataFields({ 'conversions': conversionRates });
|
||||
|
||||
const response = JSON.stringify({ conversions: conversionRates });
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ conversions: conversionRates }));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
getInitData(_blocks?: BlockExtended[]) {
|
||||
if (!_blocks) {
|
||||
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
}
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
return {
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
'conversions': priceUpdater.getLatestPrices(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
...this.extraInitProperties
|
||||
};
|
||||
}
|
||||
|
||||
handleNewStatistic(stats: OptimizedStatistic) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
@@ -258,6 +286,10 @@ class WebsocketHandler {
|
||||
|
||||
this.printLogs();
|
||||
|
||||
const response = JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
});
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -267,14 +299,12 @@ class WebsocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
}));
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
|
||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
@@ -282,7 +312,7 @@ class WebsocketHandler {
|
||||
this.printLogs();
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
|
||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||
}
|
||||
@@ -304,8 +334,47 @@ class WebsocketHandler {
|
||||
for (const deletedTx of deletedTransactions) {
|
||||
rbfCache.evict(deletedTx.txid);
|
||||
}
|
||||
memPool.removeFromSpendMap(deletedTransactions);
|
||||
memPool.addToSpendMap(newTransactions);
|
||||
const recommendedFees = feeApi.getRecommendedFee();
|
||||
|
||||
// update init data
|
||||
this.updateInitData();
|
||||
|
||||
// cache serialized objects to avoid stringify-ing the same thing for every client
|
||||
const responseCache = { ...this.initData };
|
||||
function getCachedResponse(key: string, data): string {
|
||||
if (!responseCache[key]) {
|
||||
responseCache[key] = JSON.stringify(data);
|
||||
}
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
// pre-compute new tracked outspends
|
||||
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
|
||||
const trackedTxs = new Set<string>();
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client['track-tx']) {
|
||||
trackedTxs.add(client['track-tx']);
|
||||
}
|
||||
});
|
||||
if (trackedTxs.size > 0) {
|
||||
for (const tx of newTransactions) {
|
||||
for (let i = 0; i < tx.vin.length; i++) {
|
||||
const vin = tx.vin[i];
|
||||
if (trackedTxs.has(vin.txid)) {
|
||||
if (!outspendCache[vin.txid]) {
|
||||
outspendCache[vin.txid] = { [vin.vout]: { vin: i, txid: tx.txid }};
|
||||
} else {
|
||||
outspendCache[vin.txid][vin.vout] = { vin: i, txid: tx.txid };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
|
||||
this.wss.clients.forEach(async (client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -314,17 +383,17 @@ class WebsocketHandler {
|
||||
const response = {};
|
||||
|
||||
if (client['want-stats']) {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
|
||||
response['transactions'] = getCachedResponse('transactions', latestTransactions);
|
||||
if (da?.previousTime) {
|
||||
response['da'] = da;
|
||||
response['da'] = getCachedResponse('da', da);
|
||||
}
|
||||
response['fees'] = recommendedFees;
|
||||
response['fees'] = getCachedResponse('fees', recommendedFees);
|
||||
}
|
||||
|
||||
if (client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['track-mempool-tx']) {
|
||||
@@ -332,13 +401,13 @@ class WebsocketHandler {
|
||||
if (tx) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
response['tx'] = JSON.stringify(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
response['tx'] = tx;
|
||||
response['tx'] = JSON.stringify(tx);
|
||||
}
|
||||
client['track-mempool-tx'] = null;
|
||||
}
|
||||
@@ -352,7 +421,7 @@ class WebsocketHandler {
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -366,7 +435,7 @@ class WebsocketHandler {
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -378,7 +447,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,65 +476,60 @@ class WebsocketHandler {
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const trackTxid = client['track-tx'];
|
||||
const outspends: object = {};
|
||||
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
|
||||
if (vin.txid === trackTxid) {
|
||||
outspends[vin.vout] = {
|
||||
vin: i,
|
||||
txid: tx.txid,
|
||||
};
|
||||
}
|
||||
}));
|
||||
const outspends = outspendCache[trackTxid];
|
||||
|
||||
if (Object.keys(outspends).length) {
|
||||
response['utxoSpent'] = outspends;
|
||||
if (outspends && Object.keys(outspends).length) {
|
||||
response['utxoSpent'] = JSON.stringify(outspends);
|
||||
}
|
||||
|
||||
const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
|
||||
if (rbfReplacedBy) {
|
||||
response['rbfTransaction'] = {
|
||||
response['rbfTransaction'] = JSON.stringify({
|
||||
txid: rbfReplacedBy,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const rbfChange = rbfChanges.map[client['track-tx']];
|
||||
if (rbfChange) {
|
||||
response['rbfInfo'] = rbfChanges.trees[rbfChange];
|
||||
response['rbfInfo'] = JSON.stringify(rbfChanges.trees[rbfChange]);
|
||||
}
|
||||
|
||||
const mempoolTx = newMempool[trackTxid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
response['txPosition'] = {
|
||||
response['txPosition'] = JSON.stringify({
|
||||
txid: trackTxid,
|
||||
position: mempoolTx.position,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-rbf'] === 'all' && rbfReplacements) {
|
||||
response['rbfLatest'] = rbfReplacements;
|
||||
response['rbfLatest'] = getCachedResponse('rbfLatest', rbfReplacements);
|
||||
} else if (client['track-rbf'] === 'fullRbf' && fullRbfReplacements) {
|
||||
response['rbfLatest'] = fullRbfReplacements;
|
||||
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
const serializedResponse = '{'
|
||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ '}';
|
||||
client.send(serializedResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -482,22 +546,27 @@ class WebsocketHandler {
|
||||
if (config.MEMPOOL.AUDIT) {
|
||||
let projectedBlocks;
|
||||
let auditMempool = _memPool;
|
||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && await accelerationApi.$isAcceleratedBlock(block);
|
||||
// template calculation functions have mempool side effects, so calculate audits using
|
||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||
if (separateAudit) {
|
||||
auditMempool = deepClone(_memPool);
|
||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||
}
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS && !isAccelerated)) {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||
const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const { censored, added, fresh, sigop, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||
@@ -524,6 +593,8 @@ class WebsocketHandler {
|
||||
addedTxs: added,
|
||||
missingTxs: censored,
|
||||
freshTxs: fresh,
|
||||
sigopTxs: sigop,
|
||||
acceleratedTxs: accelerated,
|
||||
matchRate: matchRate,
|
||||
});
|
||||
|
||||
@@ -539,6 +610,10 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
memPool.removeFromSpendMap(transactions);
|
||||
|
||||
// Update mempool to remove transactions included in the new block
|
||||
for (const txId of txIds) {
|
||||
delete _memPool[txId];
|
||||
@@ -546,7 +621,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
|
||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
} else {
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||
}
|
||||
@@ -556,6 +631,19 @@ class WebsocketHandler {
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const fees = feeApi.getRecommendedFee();
|
||||
|
||||
// update init data
|
||||
this.updateInitData();
|
||||
|
||||
const responseCache = { ...this.initData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
responseCache[key] = JSON.stringify(data);
|
||||
}
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -565,28 +653,27 @@ class WebsocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'da': da?.previousTime ? da : undefined,
|
||||
'fees': fees,
|
||||
};
|
||||
const response = {};
|
||||
response['block'] = getCachedResponse('block', block);
|
||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||
response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
|
||||
response['fees'] = getCachedResponse('fees', fees);
|
||||
|
||||
if (mBlocks && client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const trackTxid = client['track-tx'];
|
||||
if (txIds.indexOf(trackTxid) > -1) {
|
||||
response['txConfirmed'] = true;
|
||||
if (trackTxid && txIds.indexOf(trackTxid) > -1) {
|
||||
response['txConfirmed'] = JSON.stringify(trackTxid);
|
||||
} else {
|
||||
const mempoolTx = _memPool[trackTxid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
response['txPosition'] = {
|
||||
response['txPosition'] = JSON.stringify({
|
||||
txid: trackTxid,
|
||||
position: mempoolTx.position,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -614,7 +701,7 @@ class WebsocketHandler {
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,21 +738,24 @@ class WebsocketHandler {
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas && mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
client.send(JSON.stringify(response));
|
||||
const serializedResponse = '{'
|
||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ '}';
|
||||
client.send(serializedResponse);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,10 @@ interface IConfig {
|
||||
GEOLITE2_ASN: string;
|
||||
GEOIP2_ISP: string;
|
||||
},
|
||||
MEMPOOL_SERVICES: {
|
||||
API: string;
|
||||
ACCELERATIONS: boolean;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -258,6 +262,10 @@ const defaults: IConfig = {
|
||||
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||
},
|
||||
'MEMPOOL_SERVICES': {
|
||||
'API': '',
|
||||
'ACCELERATIONS': false,
|
||||
}
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -277,6 +285,7 @@ class Config implements IConfig {
|
||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
MAXMIND: IConfig['MAXMIND'];
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -296,6 +305,7 @@ class Config implements IConfig {
|
||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
this.MAXMIND = configs.MAXMIND;
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface BlockAudit {
|
||||
hash: string,
|
||||
missingTxs: string[],
|
||||
freshTxs: string[],
|
||||
sigopTxs: string[],
|
||||
addedTxs: string[],
|
||||
acceleratedTxs: string[],
|
||||
matchRate: number,
|
||||
}
|
||||
|
||||
@@ -84,31 +86,42 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
block: number,
|
||||
vsize: number,
|
||||
};
|
||||
acceleration?: number;
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
export interface MempoolTransactionExtended extends TransactionExtended {
|
||||
sigops: number;
|
||||
adjustedVsize: number;
|
||||
adjustedFeePerVsize: number;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize: number;
|
||||
sigops: number;
|
||||
inputs: number[];
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<number, AuditTransaction>;
|
||||
children: Set<AuditTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorWeight: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
modifiedNode: HeapNode<AuditTransaction>;
|
||||
dependencyRate?: number;
|
||||
}
|
||||
|
||||
export interface CompactThreadTransaction {
|
||||
uid: number;
|
||||
fee: number;
|
||||
weight: number;
|
||||
sigops: number;
|
||||
feePerVsize: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
inputs: number[];
|
||||
@@ -162,6 +175,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
acc?: number;
|
||||
rate?: number; // effective fee rate
|
||||
}
|
||||
|
||||
@@ -237,6 +251,11 @@ export interface EffectiveFeeStats {
|
||||
feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
|
||||
}
|
||||
|
||||
export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {
|
||||
minFee: number;
|
||||
maxFee: number;
|
||||
}
|
||||
|
||||
export interface CpfpSummary {
|
||||
transactions: TransactionExtended[];
|
||||
clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[];
|
||||
|
||||
@@ -6,20 +6,19 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
try {
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, accelerated_txs, match_rate)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||
} else {
|
||||
logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> {
|
||||
public async $getBlocksHealthHistory(div: number, interval: string | null): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
|
||||
|
||||
@@ -32,17 +31,17 @@ class BlocksAuditRepositories {
|
||||
const [rows] = await DB.query(query);
|
||||
return rows;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
logger.err(`Cannot fetch blocks health history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getPredictionsCount(): Promise<number> {
|
||||
public async $getBlocksHealthCount(): Promise<number> {
|
||||
try {
|
||||
const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
|
||||
return rows[0].count;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
logger.err(`Cannot fetch blocks health count. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -52,9 +51,10 @@ class BlocksAuditRepositories {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
||||
blocks.weight, blocks.tx_count,
|
||||
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
|
||||
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, accelerated_txs as acceleratedTxs, match_rate as matchRate
|
||||
FROM blocks_audits
|
||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||
WHERE blocks_audits.hash = "${hash}"
|
||||
`);
|
||||
@@ -63,6 +63,8 @@ class BlocksAuditRepositories {
|
||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
|
||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
||||
rows[0].template = JSON.parse(rows[0].template);
|
||||
|
||||
|
||||
@@ -36,17 +36,16 @@ class BlocksSummariesRepository {
|
||||
try {
|
||||
const transactions = JSON.stringify(params.template?.transactions || []);
|
||||
await DB.query(`
|
||||
INSERT INTO blocks_summaries (height, id, transactions, template)
|
||||
VALUE (?, ?, ?, ?)
|
||||
INSERT INTO blocks_templates (id, template)
|
||||
VALUE (?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
template = ?
|
||||
`, [params.height, blockId, '[]', transactions, transactions]);
|
||||
`, [blockId, transactions, transactions]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
|
||||
} else {
|
||||
logger.debug(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
throw e;
|
||||
logger.warn(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
contributors/vostrnad.txt
Normal file
3
contributors/vostrnad.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: vostrnad
|
||||
@@ -27,7 +27,9 @@
|
||||
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
|
||||
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
|
||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__CORE_RPC_HOST__",
|
||||
@@ -61,7 +63,7 @@
|
||||
"DATABASE": "__DATABASE_DATABASE__",
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": "__DATABASE_TIMEOUT__"
|
||||
"TIMEOUT": __DATABASE_TIMEOUT__
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": __SYSLOG_ENABLED__,
|
||||
@@ -84,13 +86,15 @@
|
||||
"STATS_REFRESH_INTERVAL": __LIGHTNING_STATS_REFRESH_INTERVAL__,
|
||||
"GRAPH_REFRESH_INTERVAL": __LIGHTNING_GRAPH_REFRESH_INTERVAL__,
|
||||
"LOGGER_UPDATE_INTERVAL": __LIGHTNING_LOGGER_UPDATE_INTERVAL__,
|
||||
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__"
|
||||
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
|
||||
"FORENSICS_INTERVAL": __LIGHTNING_FORENSICS_INTERVAL__,
|
||||
"FORENSICS_RATE_LIMIT": __LIGHTNING_FORENSICS_RATE_LIMIT__
|
||||
},
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
|
||||
"MACAROON_PATH": "__LND_MACAROON_PATH__",
|
||||
"REST_API_URL": "__LND_REST_API_URL__",
|
||||
"TIMEOUT": "__LND_TIMEOUT__"
|
||||
"TIMEOUT": __LND_TIMEOUT__
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
@@ -120,5 +124,9 @@
|
||||
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
|
||||
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
|
||||
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
|
||||
},
|
||||
"MEMPOOL_SERVICES": {
|
||||
"API": "__MEMPOOL_SERVICES_API__",
|
||||
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
|
||||
|
||||
# ESPLORA
|
||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=null}
|
||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
|
||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
|
||||
|
||||
# SECOND_CORE_RPC
|
||||
@@ -108,6 +108,8 @@ __LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
|
||||
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
|
||||
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
|
||||
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
|
||||
__LIGHTNING_FORENSICS_INTERVAL__=${LIGHTNING_FORENSICS_INTERVAL:=43200}
|
||||
__LIGHTNING_FORENSICS_RATE_LIMIT__=${LIGHTNING_FORENSICS_RATE_LIMIT:=20}
|
||||
|
||||
# LND
|
||||
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
|
||||
@@ -124,31 +126,35 @@ __MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City
|
||||
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
|
||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
__MEMPOOL_SERVICES_API__==${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__==${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_ENABLED__/${__MEMPOOL_ENABLED__}/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_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!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_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_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__/${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_INITIAL_BLOCKS_AMOUNT__!${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__!${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_INDEXING_BLOCKS_AMOUNT__!${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__!${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__!${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!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_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_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_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_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||
@@ -158,53 +164,53 @@ sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-conf
|
||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json
|
||||
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
|
||||
sed -i "s/__CORE_RPC_TIMEOUT__/${__CORE_RPC_TIMEOUT__}/g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!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_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json
|
||||
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json
|
||||
sed -i "s/__SECOND_CORE_RPC_TIMEOUT__/${__SECOND_CORE_RPC_TIMEOUT__}/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_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
|
||||
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_SOCKET__!${__DATABASE_SOCKET__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PORT__!${__DATABASE_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_DATABASE__!${__DATABASE_DATABASE__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__DATABASE_PORT__/${__DATABASE_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__DATABASE_DATABASE__/${__DATABASE_DATABASE__}/g" mempool-config.json
|
||||
sed -i "s/__DATABASE_USERNAME__/${__DATABASE_USERNAME__}/g" mempool-config.json
|
||||
sed -i "s/__DATABASE_PASSWORD__/${__DATABASE_PASSWORD__}/g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_PORT__!${__SYSLOG_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_MIN_PRIORITY__!${__SYSLOG_MIN_PRIORITY__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_FACILITY__!${__SYSLOG_FACILITY__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__SYSLOG_ENABLED__/${__SYSLOG_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s/__SYSLOG_HOST__/${__SYSLOG_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__SYSLOG_PORT__/${__SYSLOG_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__SYSLOG_MIN_PRIORITY__/${__SYSLOG_MIN_PRIORITY__}/g" mempool-config.json
|
||||
sed -i "s/__SYSLOG_FACILITY__/${__SYSLOG_FACILITY__}/g" mempool-config.json
|
||||
sed -i "s!__STATISTICS_ENABLED__!${__STATISTICS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__!${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__STATISTICS_ENABLED__/${__STATISTICS_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s/__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__/${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}/g" mempool-config.json
|
||||
|
||||
sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s!__BISQ_ENABLED__!${__BISQ_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_ENABLED__!${__SOCKS5PROXY_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_USE_ONION__!${__SOCKS5PROXY_USE_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_HOST__!${__SOCKS5PROXY_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_PORT__!${__SOCKS5PROXY_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
|
||||
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
|
||||
@@ -223,6 +229,8 @@ sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" memp
|
||||
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_FORENSICS_INTERVAL__!${__LIGHTNING_FORENSICS_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_FORENSICS_RATE_LIMIT__!${__LIGHTNING_FORENSICS_RATE_LIMIT__}!g" mempool-config.json
|
||||
|
||||
# LND
|
||||
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
|
||||
@@ -239,5 +247,9 @@ sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-conf
|
||||
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
|
||||
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -504,9 +504,17 @@ describe('Mainnet', () => {
|
||||
|
||||
describe('RBF transactions', () => {
|
||||
it('shows RBF transactions properly (mobile)', () => {
|
||||
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
|
||||
fixture: 'mainnet_tx_cached.json'
|
||||
}).as('cached_tx');
|
||||
|
||||
cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
|
||||
fixture: 'mainnet_rbf_new.json'
|
||||
}).as('rbf');
|
||||
|
||||
cy.viewport('iphone-xr');
|
||||
cy.mockMempoolSocket();
|
||||
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
|
||||
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
|
||||
|
||||
cy.waitForSkeletonGone();
|
||||
|
||||
@@ -524,22 +532,30 @@ describe('Mainnet', () => {
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('.alert-mempool').should('be.visible');
|
||||
cy.get('.alert-mempool').invoke('css', 'width').then((alertWidth) => {
|
||||
cy.get('.alert').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-success').then(getRectangle).then((rectA) => {
|
||||
cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
|
||||
cy.get('.btn-danger').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)', () => {
|
||||
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
|
||||
fixture: 'mainnet_tx_cached.json'
|
||||
}).as('cached_tx');
|
||||
|
||||
cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
|
||||
fixture: 'mainnet_rbf_new.json'
|
||||
}).as('rbf');
|
||||
|
||||
cy.viewport('macbook-16');
|
||||
cy.mockMempoolSocket();
|
||||
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
|
||||
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
|
||||
|
||||
cy.waitForSkeletonGone();
|
||||
|
||||
@@ -557,17 +573,17 @@ describe('Mainnet', () => {
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('.alert-mempool').should('be.visible');
|
||||
cy.get('.alert').should('be.visible');
|
||||
|
||||
const alertLocator = '.alert-mempool';
|
||||
const alertLocator = '.alert';
|
||||
const tableLocator = '.container-xl > :nth-child(3)';
|
||||
|
||||
cy.get(tableLocator).invoke('css', 'width').then((firstWidth) => {
|
||||
cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth);
|
||||
});
|
||||
|
||||
cy.get('.btn-success').then(getRectangle).then((rectA) => {
|
||||
cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
|
||||
cy.get('.btn-danger').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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,52 +1,4 @@
|
||||
{
|
||||
"rbfTransaction": {
|
||||
"txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46",
|
||||
"version": 2,
|
||||
"locktime": 632699,
|
||||
"vin": [
|
||||
{
|
||||
"txid": "02238126a63ea2669c5f378012180ef8b54402a949316f9b2f1352c51730a086",
|
||||
"vout": 0,
|
||||
"prevout": {
|
||||
"scriptpubkey": "a914f8e495456956c833e5e8c69b9a9dc041aa14c72f87",
|
||||
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 f8e495456956c833e5e8c69b9a9dc041aa14c72f OP_EQUAL",
|
||||
"scriptpubkey_type": "p2sh",
|
||||
"scriptpubkey_address": "3QP3LMD8veT5GtWV83Nosif2Bhr73857VB",
|
||||
"value": 25000000
|
||||
},
|
||||
"scriptsig": "22002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
|
||||
"scriptsig_asm": "OP_PUSHBYTES_34 002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
|
||||
"witness": [
|
||||
"",
|
||||
"3044022009e2d3a8e645f65bc89c8492cd9c08e6fb02609fd402214884a754a1970145340220575bb325429def59f3a3f1e22d9740a3feecbe97438ff3bb5796b2c46b3c477f01",
|
||||
"3044022039c34372882da8fc1c1243bd72b5e7e5e6870301ef56bdebb87bc647fb50f9b5022071a704ee77d742f78b10e45be675d4c45a5f31e884139e75c975144fde70e41701",
|
||||
"522102346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf70421021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b52ae"
|
||||
],
|
||||
"is_coinbase": false,
|
||||
"sequence": 4294967293,
|
||||
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 43288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
|
||||
"inner_witnessscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 02346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf704 OP_PUSHBYTES_33 021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b OP_PUSHNUM_2 OP_CHECKMULTISIG"
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey": "a914fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f87",
|
||||
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f OP_EQUAL",
|
||||
"scriptpubkey_type": "p2sh",
|
||||
"scriptpubkey_address": "3QnNmDhZS7toHA7bhhbTPBdtpLJoeecq5c",
|
||||
"value": 13986350
|
||||
},
|
||||
{
|
||||
"scriptpubkey": "76a914edc93d0446deec1c2d514f3a490f050096e74e0e88ac",
|
||||
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 edc93d0446deec1c2d514f3a490f050096e74e0e OP_EQUALVERIFY OP_CHECKSIG",
|
||||
"scriptpubkey_type": "p2pkh",
|
||||
"scriptpubkey_address": "1NgJDkTUqJxxCAAZrrsC87kWag5kphrRtM",
|
||||
"value": 11000000
|
||||
}
|
||||
],
|
||||
"size": 372,
|
||||
"weight": 828,
|
||||
"fee": 1.5,
|
||||
"status": { "confirmed": false }
|
||||
}
|
||||
}
|
||||
"txReplaced": {
|
||||
"txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46"
|
||||
}}
|
||||
31
frontend/cypress/fixtures/mainnet_rbf_new.json
Normal file
31
frontend/cypress/fixtures/mainnet_rbf_new.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"replacements": {
|
||||
"tx": {
|
||||
"txid": "f22735aaa8eb84bcae3e7705f78609c6f5f0cd7dfc34ae03094e61f2dab0cc64",
|
||||
"fee": 13843,
|
||||
"vsize": 109.25,
|
||||
"value": 253003805,
|
||||
"rate": 36.04666732302845,
|
||||
"rbf": true
|
||||
},
|
||||
"time": 1683865345,
|
||||
"fullRbf": false,
|
||||
"replaces": [
|
||||
{
|
||||
"tx": {
|
||||
"txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
|
||||
"fee": 8794,
|
||||
"vsize": 109.25,
|
||||
"value": 253008854,
|
||||
"rate": 35.05247612484001,
|
||||
"rbf": true
|
||||
},
|
||||
"time": 1683864993,
|
||||
"interval": 352,
|
||||
"fullRbf": false,
|
||||
"replaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"replaces": null
|
||||
}
|
||||
60
frontend/cypress/fixtures/mainnet_tx_cached.json
Normal file
60
frontend/cypress/fixtures/mainnet_tx_cached.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"vsize": 109,
|
||||
"feePerVsize": 80.49427917620137,
|
||||
"effectiveFeePerVsize": 35.05247612484001,
|
||||
"txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
|
||||
"version": 2,
|
||||
"locktime": 0,
|
||||
"vin": [
|
||||
{
|
||||
"txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
|
||||
"vout": 144,
|
||||
"prevout": {
|
||||
"scriptpubkey": "0014d98654186b90d95da7e31a30929f5b5b6a0af250",
|
||||
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d98654186b90d95da7e31a30929f5b5b6a0af250",
|
||||
"scriptpubkey_type": "v0_p2wpkh",
|
||||
"scriptpubkey_address": "bc1qmxr9gxrtjrv4mflrrgcf986mtd4q4ujss432tk",
|
||||
"value": 253017648
|
||||
},
|
||||
"scriptsig": "",
|
||||
"scriptsig_asm": "",
|
||||
"witness": [
|
||||
"30440220448e8f58fcdea87c1969d58438b49da5b43712380bc4c68b02d22cf6b164907302207b2ed660f1a5b3b74f712961ffb3f3a7d1ac6e48b269ea6ff15df985042211f301",
|
||||
"02e39a1f3583e382cec1a1fab6a3f5950b6403c953fada58d809127a497f502ebe"
|
||||
],
|
||||
"is_coinbase": false,
|
||||
"sequence": 4294967293
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey": "0014edb5167da7e97c73d7931eb2130ac3e34e6845a9",
|
||||
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 edb5167da7e97c73d7931eb2130ac3e34e6845a9",
|
||||
"scriptpubkey_type": "v0_p2wpkh",
|
||||
"scriptpubkey_address": "bc1qak63vld8a97884unr6epxzkrud8xs3dfdqswy2",
|
||||
"value": 253008854
|
||||
}
|
||||
],
|
||||
"size": 191,
|
||||
"weight": 437,
|
||||
"fee": 8794,
|
||||
"status": {
|
||||
"confirmed": false
|
||||
},
|
||||
"firstSeen": 1683864993,
|
||||
"uid": 298353,
|
||||
"position": {
|
||||
"block": 0,
|
||||
"vsize": 886207.5
|
||||
},
|
||||
"cpfpChecked": true,
|
||||
"ancestors": [
|
||||
{
|
||||
"txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
|
||||
"fee": 169220,
|
||||
"weight": 19877
|
||||
}
|
||||
],
|
||||
"descendants": [],
|
||||
"bestDescendant": null
|
||||
}
|
||||
@@ -29,6 +29,14 @@ export const mempoolFeeColors = [
|
||||
'ba3243',
|
||||
'b92b48',
|
||||
'b9254b',
|
||||
'b8214d',
|
||||
'b71d4f',
|
||||
'b61951',
|
||||
'b41453',
|
||||
'b30e55',
|
||||
'b10857',
|
||||
'b00259',
|
||||
'ae005b',
|
||||
];
|
||||
|
||||
export const chartColors = [
|
||||
@@ -69,6 +77,7 @@ export const chartColors = [
|
||||
"#3E2723",
|
||||
"#212121",
|
||||
"#263238",
|
||||
"#801313",
|
||||
];
|
||||
|
||||
export const poolsColor = {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
.pagination-container {
|
||||
float: none;
|
||||
margin-bottom: 200px;
|
||||
@media(min-width: 400px){
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
padding-bottom: 110px;
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
<h5 class="card-title">US Dollar - BTC/USD</h5>
|
||||
<div class="chart-container">
|
||||
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
|
||||
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
|
||||
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -105,9 +105,6 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<app-language-selector></app-language-selector>
|
||||
|
||||
<app-global-footer></app-global-footer>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
@@ -124,4 +121,4 @@
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="skeleton-loader shorter"></div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
</span>
|
||||
<span class="grow"></span>
|
||||
<div class="container-buttons">
|
||||
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
<div *ngIf="(latestBlock$ | async) as latestBlock">
|
||||
<app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -70,11 +70,7 @@
|
||||
|
||||
<div class="btn-container">
|
||||
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button type="button" class="btn btn-sm btn-success mt-2">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
|
||||
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="container-xl about-page">
|
||||
|
||||
<div class="intro">
|
||||
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">™</span>
|
||||
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">®</span>
|
||||
<img class="logo" src="/resources/mempool-logo-bigger.png" />
|
||||
<div class="version">
|
||||
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
|
||||
@@ -13,7 +13,7 @@
|
||||
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||
</div>
|
||||
|
||||
<video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
|
||||
<video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
|
||||
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
|
||||
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
|
||||
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
|
||||
@@ -119,6 +119,18 @@
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)" fill-rule="evenodd" clip-rule="evenodd" fill="#e21924">
|
||||
<path d="M21.92 14.59a1.18 1.18 0 0 0-1.18-1.18h-1.82v2.36h1.82a1.18 1.18 0 0 0 1.18-1.18ZM21 17.07h-2v2.45h2a1.23 1.23 0 1 0 0-2.45Z"/>
|
||||
<path d="M36.43 0 35 5.59l-8 2.64-2.43-3.61-4.74 2.05-4.74-2.05-2.43 3.61-8-2.64L3.21 0 0 7.86l7.89 5.86-5.56 4 5.56 1.12 2.69-.49v3.17l3.59 4.38.68 3.19 5 2.87 5-2.87.68-3.19 3.59-4.38v-3.17l2.7.49 5.56-1.12-5.56-4 7.89-5.86zM24.69 18.45a2.5 2.5 0 0 1-2.5 2.5h-1.11v1.56h-1.26V21h-.9v1.56h-1.27V21H15.3v-1.42h.64a.9.9 0 0 0 .9-.9V14.3a.901.901 0 0 0-.9-.91h-.64V12h2.35v-1.5h1.27V12h.9v-1.5h1.26V12h.68A2.269 2.269 0 0 1 24 14.31a2.25 2.25 0 0 1-.92 1.82 2.52 2.52 0 0 1 1.58 2.32z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a"><path fill="#fff" d="M0 0h160v32H0z"/></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
@@ -205,9 +217,9 @@
|
||||
<img class="image" src="/resources/profile/nix-bitcoin.png" />
|
||||
<span>NixOS</span>
|
||||
</a>
|
||||
<a href="https://github.com/Start9Labs/embassy-os" target="_blank" title="EmbassyOS">
|
||||
<a href="https://github.com/Start9Labs/start-os" target="_blank" title="StartOS">
|
||||
<img class="image" src="/resources/profile/start9.png" />
|
||||
<span>EmbassyOS</span>
|
||||
<span>StartOS</span>
|
||||
</a>
|
||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
|
||||
<img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
|
||||
@@ -384,7 +396,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project™, mempool.space™, the mempool logo™, the mempool.space logos™, the mempool square logo™, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
@@ -393,27 +405,6 @@
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
<div class="social-icons">
|
||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://twitter.com/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://youtube.com/@mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-version" *ngIf="officialMempoolSpace">
|
||||
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -17,6 +17,7 @@ import { DOCUMENT } from '@angular/common';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AboutComponent implements OnInit {
|
||||
@ViewChild('promoVideo') promoVideo: ElementRef;
|
||||
backendInfo$: Observable<IBackendInfo>;
|
||||
sponsors$: Observable<any>;
|
||||
translators$: Observable<ITranslators>;
|
||||
@@ -91,7 +92,11 @@ export class AboutComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
showSubtitles(language) {
|
||||
showSubtitles(language): boolean {
|
||||
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
|
||||
}
|
||||
|
||||
unmutePromoVideo(): void {
|
||||
this.promoVideo.nativeElement.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@
|
||||
|
||||
<br />
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -17,6 +17,12 @@ li.nav-item {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
footer > .container-fluid {
|
||||
padding-bottom: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.navbar {
|
||||
padding: 0rem 2rem;
|
||||
|
||||
@@ -17,6 +17,7 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -31,6 +32,11 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.mainnet.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="full-container">
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</span>
|
||||
<span i18n="mining.blocks-health">Block Health</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
@@ -12,34 +12,34 @@
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
@@ -13,9 +13,9 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-prediction-graph',
|
||||
templateUrl: './block-prediction-graph.component.html',
|
||||
styleUrls: ['./block-prediction-graph.component.scss'],
|
||||
selector: 'app-block-health-graph',
|
||||
templateUrl: './block-health-graph.component.html',
|
||||
styleUrls: ['./block-health-graph.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
@@ -26,7 +26,7 @@ import { StateService } from '../../services/state.service';
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockPredictionGraphComponent implements OnInit {
|
||||
export class BlockHealthGraphComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
@@ -60,7 +60,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Prediction Accuracy`);
|
||||
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
|
||||
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
@@ -80,7 +80,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
this.timespan = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalBlockPrediction$(timespan)
|
||||
return this.apiService.getHistoricalBlocksHealth$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.prepareChartOptions(response.body);
|
||||
@@ -163,7 +163,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
hideOverlap: true,
|
||||
padding: [0, 5],
|
||||
},
|
||||
data: data.map(prediction => prediction[0])
|
||||
data: data.map(health => health[0])
|
||||
},
|
||||
yAxis: data.length === 0 ? undefined : [
|
||||
{
|
||||
@@ -186,12 +186,12 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
series: data.length === 0 ? undefined : [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: $localize`Match rate`,
|
||||
data: data.map(prediction => ({
|
||||
value: prediction[2],
|
||||
block: prediction[1],
|
||||
name: $localize`Health`,
|
||||
data: data.map(health => ({
|
||||
value: health[2],
|
||||
block: health[1],
|
||||
itemStyle: {
|
||||
color: this.getPredictionColor(prediction[2])
|
||||
color: this.getHealthColor(health[2])
|
||||
}
|
||||
})),
|
||||
type: 'bar',
|
||||
@@ -257,7 +257,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
|
||||
}
|
||||
|
||||
getPredictionColor(matchRate) {
|
||||
getHealthColor(matchRate) {
|
||||
return this.colorGradient(
|
||||
Math.pow((100 - matchRate) / 100, 0.5),
|
||||
{red: 67, green: 171, blue: 71},
|
||||
@@ -294,7 +294,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
excludeComponents: ['dataZoom'],
|
||||
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
}), `block-health-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
@@ -133,7 +133,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
if (this.scene) {
|
||||
this.scene.update(add, remove, change, direction, resetLayout);
|
||||
this.start();
|
||||
|
||||
@@ -156,7 +156,7 @@ export default class BlockScene {
|
||||
this.updateAll(startTime, 200, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
const startTime = performance.now();
|
||||
const removed = this.removeBatch(remove, startTime, direction);
|
||||
|
||||
@@ -181,6 +181,7 @@ export default class BlockScene {
|
||||
// update effective rates
|
||||
change.forEach(tx => {
|
||||
if (this.txs[tx.txid]) {
|
||||
this.txs[tx.txid].acc = tx.acc;
|
||||
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
|
||||
this.txs[tx.txid].rate = tx.rate;
|
||||
this.txs[tx.txid].dirty = true;
|
||||
|
||||
@@ -16,6 +16,7 @@ const auditColors = {
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
@@ -36,8 +37,9 @@ export default class TxView implements TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
feerate: number;
|
||||
acc?: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -60,6 +62,7 @@ export default class TxView implements TransactionStripped {
|
||||
this.vsize = tx.vsize;
|
||||
this.value = tx.value;
|
||||
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.initialised = false;
|
||||
@@ -164,6 +167,11 @@ export default class TxView implements TransactionStripped {
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!this.scene?.highlightingEnabled) {
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
@@ -171,6 +179,7 @@ export default class TxView implements TransactionStripped {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
return auditColors.missing;
|
||||
@@ -178,6 +187,8 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (this.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
@@ -185,7 +196,11 @@ export default class TxView implements TransactionStripped {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
return feeLevelColor;
|
||||
if (this.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td>
|
||||
{{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
@@ -44,9 +45,11 @@
|
||||
<td *ngSwitchCase="'found'"><span class="badge badge-success" i18n="transaction.audit.match">Match</span></td>
|
||||
<td *ngSwitchCase="'censored'"><span class="badge badge-danger" i18n="transaction.audit.removed">Removed</span></td>
|
||||
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
|
||||
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
vsize = 1;
|
||||
feeRate = 0;
|
||||
effectiveRate;
|
||||
acceleration;
|
||||
|
||||
tooltipPosition: Position = { x: 0, y: 0 };
|
||||
|
||||
@@ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.vsize = tx.vsize || 1;
|
||||
this.feeRate = this.fee / this.vsize;
|
||||
this.effectiveRate = tx.rate;
|
||||
this.acceleration = tx.acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isAccelerated = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -354,6 +356,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const txid of blockAudit.freshTxs || []) {
|
||||
isFresh[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.sigopTxs || []) {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.acceleratedTxs || []) {
|
||||
isAccelerated[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
tx.context = 'projected';
|
||||
@@ -362,10 +370,13 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
} else if (inBlock[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
|
||||
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||
tx.context = 'actual';
|
||||
@@ -380,6 +391,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
|
||||
@@ -26,7 +26,6 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
this.updateTime();
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
<div class="stats top right">
|
||||
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
|
||||
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee + 300 }} sat/vB</p>
|
||||
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee }} sat/vB</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
|
||||
<p [innerHTML]="block.size | bytes: 2"></p>
|
||||
|
||||
@@ -52,7 +52,7 @@ export class ClockComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resizeCanvas();
|
||||
this.websocketService.want(['blocks']);
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
|
||||
<div class="fee-estimation-wrapper" *ngIf="(isLoading$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
|
||||
<div class="d-flex">
|
||||
<div class="fee-progress-bar" [style.background]="noPriority">
|
||||
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { Recommendedfees } from '../../interfaces/websocket.interface';
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { map, startWith, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fees-box',
|
||||
@@ -12,7 +12,7 @@ import { tap } from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FeesBoxComponent implements OnInit {
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
isLoading$: Observable<boolean>;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
|
||||
noPriority = '#2e324e';
|
||||
@@ -22,7 +22,12 @@ export class FeesBoxComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.isLoading$ = combineLatest(
|
||||
this.stateService.isLoadingWebSocket$.pipe(startWith(false)),
|
||||
this.stateService.loadingIndicators$.pipe(startWith({ mempool: 0 })),
|
||||
).pipe(map(([socket, indicators]) => {
|
||||
return socket || (indicators.mempool != null && indicators.mempool !== 100);
|
||||
}));
|
||||
this.recommendedFees$ = this.stateService.recommendedFees$
|
||||
.pipe(
|
||||
tap((fees) => {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
|
||||
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</a>
|
||||
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</ng-container>
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
@@ -92,10 +92,10 @@
|
||||
|
||||
<app-testnet-alert *ngIf="network.val === 'liquidtestnet'"></app-testnet-alert>
|
||||
|
||||
<br />
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<app-global-footer></app-global-footer>
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
</ng-container>
|
||||
@@ -19,6 +19,7 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
network$: Observable<string>;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -27,13 +28,18 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.liquid.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +47,7 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</ng-container>
|
||||
</a>
|
||||
|
||||
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
@@ -64,9 +64,9 @@
|
||||
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
|
||||
<main style="margin-top: 24px;">
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-global-footer></app-global-footer>
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
</ng-container>
|
||||
|
||||
@@ -20,6 +20,8 @@ export class MasterPageComponent implements OnInit {
|
||||
urlLanguage: string;
|
||||
subdomain = '';
|
||||
networkPaths: { [network: string]: string };
|
||||
networkPaths$: Observable<Record<string, string>>;
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -28,7 +30,7 @@ export class MasterPageComponent implements OnInit {
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
@@ -36,6 +38,11 @@ export class MasterPageComponent implements OnInit {
|
||||
this.subdomain = this.enterpriseService.getSubdomain();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.mainnet.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +50,7 @@ export class MasterPageComponent implements OnInit {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
updateBlock(delta: MempoolBlockDelta): void {
|
||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
||||
|
||||
if (this.blockIndex !== this.index) {
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
||||
this.blockGraph.replace(delta.added, direction);
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }"></div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -169,4 +169,34 @@
|
||||
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
||||
border-radius: 2px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.blink{
|
||||
width:400px;
|
||||
height:400px;
|
||||
border-bottom: 35px solid #FFF;
|
||||
animation: blink 0.2s infinite;
|
||||
}
|
||||
@keyframes blink{
|
||||
0% {
|
||||
border-bottom: 35px solid green;
|
||||
}
|
||||
50% {
|
||||
border-bottom: 35px solid yellow;
|
||||
}
|
||||
100% {
|
||||
border-bottom: 35px solid orange;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes blink{
|
||||
0% {
|
||||
border-bottom: 35px solid green;
|
||||
}
|
||||
50% {
|
||||
border-bottom: 35px solid yellow;
|
||||
}
|
||||
100% {
|
||||
border-bottom: 35px solid orange;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
||||
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() minimal: boolean = false;
|
||||
@Input() blockWidth: number = 125;
|
||||
@Input() containerWidth: number = null;
|
||||
@Input() count: number = null;
|
||||
@Input() spotlight: number = 0;
|
||||
|
||||
@@ -104,7 +105,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.mempoolEmptyBlocks.forEach((b) => {
|
||||
this.mempoolEmptyBlockStyles.push(this.getStyleForMempoolEmptyBlock(b.index));
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolEmptyBlocks);
|
||||
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
|
||||
|
||||
this.mempoolBlocks.map(() => {
|
||||
this.updateMempoolBlockStyles();
|
||||
@@ -244,14 +245,35 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.animateEntry = false;
|
||||
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
|
||||
}
|
||||
|
||||
trackByFn(index: number, block: MempoolBlock) {
|
||||
return (block.isStack) ? `stack-${block.index}` : block.index;
|
||||
}
|
||||
|
||||
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||
while (blocks.length < blocksAmount) {
|
||||
blocks.push({
|
||||
blockSize: 0,
|
||||
blockVSize: 0,
|
||||
feeRange: [],
|
||||
index: blocks.length,
|
||||
medianFee: 0,
|
||||
nTx: 0,
|
||||
totalFees: 0
|
||||
});
|
||||
}
|
||||
while (blocks.length > blocksAmount) {
|
||||
blocks.pop();
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
let blocksAmount;
|
||||
if (this.count) {
|
||||
blocksAmount = 8;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
|
||||
})
|
||||
export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any[];
|
||||
@Input() limitFee = 350;
|
||||
@Input() filterSize = 100000;
|
||||
@Input() limitFilterFee = 1;
|
||||
@Input() height: number | string = 200;
|
||||
@Input() top: number | string = 20;
|
||||
@@ -41,7 +41,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
};
|
||||
windowPreference: string;
|
||||
hoverIndexSerie = 0;
|
||||
maxFee: number;
|
||||
feeLimitIndex: number;
|
||||
maxFeeIndex: number;
|
||||
feeLevelsOrdered = [];
|
||||
chartColorsOrdered = chartColors;
|
||||
inverted: boolean;
|
||||
@@ -101,14 +103,20 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
generateArray(mempoolStats: OptimizedMempoolStats[]) {
|
||||
const finalArray: number[][][] = [];
|
||||
let feesArray: number[][] = [];
|
||||
const limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
|
||||
for (let index = limitFeesTemplate; index > -1; index--) {
|
||||
|
||||
let maxTier = 0;
|
||||
for (let index = 37; index > -1; index--) {
|
||||
feesArray = [];
|
||||
mempoolStats.forEach((stats) => {
|
||||
if (stats.vsizes[index] >= this.filterSize) {
|
||||
maxTier = Math.max(maxTier, index);
|
||||
}
|
||||
feesArray.push([stats.added * 1000, stats.vsizes[index] ? stats.vsizes[index] : 0]);
|
||||
});
|
||||
finalArray.push(feesArray);
|
||||
}
|
||||
this.maxFeeIndex = maxTier;
|
||||
|
||||
finalArray.reverse();
|
||||
return finalArray;
|
||||
}
|
||||
@@ -121,7 +129,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
const newColors = [];
|
||||
for (let index = 0; index < series.length; index++) {
|
||||
const value = series[index];
|
||||
if (index >= this.feeLimitIndex) {
|
||||
if (index >= this.feeLimitIndex && index <= this.maxFeeIndex) {
|
||||
newColors.push(this.chartColorsOrdered[index]);
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
@@ -178,7 +186,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
alwaysShowContent: false,
|
||||
position: (pos, params, el, elRect, size) => {
|
||||
const positions = { top: (this.template === 'advanced') ? 0 : -30 };
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 60;
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
|
||||
return positions;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
|
||||
@@ -186,10 +194,19 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
type: 'cross',
|
||||
label: {
|
||||
formatter: (params: any) => {
|
||||
if (params.axisDimension === 'y') {
|
||||
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true)
|
||||
} else {
|
||||
return formatterXAxis(this.locale, this.windowPreference, params.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const { totalValue, totalValueArray } = this.getTotalValues(params);
|
||||
const itemFormatted = [];
|
||||
let totalParcial = 0;
|
||||
@@ -371,15 +388,24 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
orderLevels() {
|
||||
this.feeLevelsOrdered = [];
|
||||
const maxIndex = Math.min(feeLevels.length, this.maxFeeIndex);
|
||||
for (let i = 0; i < feeLevels.length; i++) {
|
||||
if (feeLevels[i] === this.limitFilterFee) {
|
||||
this.feeLimitIndex = i;
|
||||
}
|
||||
if (feeLevels[i] <= this.limitFee) {
|
||||
if (feeLevels[i] <= feeLevels[this.maxFeeIndex]) {
|
||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
|
||||
if (i === maxIndex || feeLevels[i] == null) {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)}+`);
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
|
||||
}
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
|
||||
if (i === maxIndex || feeLevels[i] == null) {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]}+`);
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = this.regexAddress.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
|
||||
if (matchesAddress && this.network === 'bisq') {
|
||||
searchText = 'B' + searchText;
|
||||
@@ -198,7 +198,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
if (this.regexAddress.test(searchText)) {
|
||||
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
|
||||
this.navigate('/address/', searchText);
|
||||
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
|
||||
this.navigate('/block/', searchText);
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
|
||||
<ul>
|
||||
<ng-template ngFor let-feeData let-i="index" [ngForOf]="feeLevelDropdownData">
|
||||
<ng-template [ngIf]="feeData.fee <= 400">
|
||||
<ng-template [ngIf]="feeData.fee <= (feeLevels[maxFeeIndex])">
|
||||
<li (click)="filterFeeIndex = feeData.fee"
|
||||
[class]="filterFeeIndex > feeData.fee ? 'inactive' : ''">
|
||||
<span class="square" [ngStyle]="{'backgroundColor': feeData.color}"></span>
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="incoming-transactions-graph">
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [limitFee]="500"
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'"
|
||||
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
|
||||
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,9 @@ export class StatisticsComponent implements OnInit {
|
||||
spinnerLoading = false;
|
||||
feeLevels = feeLevels;
|
||||
chartColors = chartColors;
|
||||
filterSize = 100000;
|
||||
filterFeeIndex = 1;
|
||||
maxFeeIndex: number;
|
||||
dropDownOpen = false;
|
||||
|
||||
mempoolStats: OptimizedMempoolStats[] = [];
|
||||
@@ -134,6 +136,16 @@ export class StatisticsComponent implements OnInit {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
|
||||
let maxTier = 0;
|
||||
for (let index = 37; index > -1; index--) {
|
||||
mempoolStats.forEach((stats) => {
|
||||
if (stats.vsizes[index] >= this.filterSize) {
|
||||
maxTier = Math.max(maxTier, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.maxFeeIndex = maxTier;
|
||||
|
||||
this.capExtremeVbytesValues();
|
||||
|
||||
this.mempoolTransactionsWeightPerSecondData = {
|
||||
@@ -152,27 +164,42 @@ export class StatisticsComponent implements OnInit {
|
||||
}
|
||||
|
||||
setFeeLevelDropdownData() {
|
||||
let _feeLevels = feeLevels
|
||||
let _feeLevels = feeLevels;
|
||||
let _chartColors = chartColors;
|
||||
if (!this.inverted) {
|
||||
_feeLevels = [...feeLevels].reverse();
|
||||
_chartColors = [...chartColors].reverse();
|
||||
}
|
||||
_feeLevels.forEach((fee, i) => {
|
||||
let range;
|
||||
const nextIndex = this.inverted ? i + 1 : i - 1;
|
||||
if (this.stateService.isLiquid()) {
|
||||
if (_feeLevels[nextIndex] == null) {
|
||||
range = `${(_feeLevels[i] / 10).toFixed(1)}+`;
|
||||
} else {
|
||||
range = `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[nextIndex] / 10).toFixed(1)}`;
|
||||
}
|
||||
} else {
|
||||
if (_feeLevels[nextIndex] == null) {
|
||||
range = `${_feeLevels[i]}+`;
|
||||
} else {
|
||||
range = `${_feeLevels[i]} - ${_feeLevels[nextIndex]}`;
|
||||
}
|
||||
}
|
||||
if (this.inverted) {
|
||||
this.feeLevelDropdownData.push({
|
||||
fee: fee,
|
||||
range: this.stateService.isLiquid() ? `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[i + 1] / 10).toFixed(1)}` : `${_feeLevels[i]} - ${_feeLevels[i + 1]}`,
|
||||
range,
|
||||
color: _chartColors[i],
|
||||
});
|
||||
} else {
|
||||
this.feeLevelDropdownData.push({
|
||||
fee: fee,
|
||||
range: this.stateService.isLiquid() ? `${(_feeLevels[i] / 10).toFixed(1)} - ${(_feeLevels[i - 1] / 10).toFixed(1)}` : `${_feeLevels[i]} - ${_feeLevels[i - 1]}`,
|
||||
color: _chartColors[i - 1],
|
||||
range,
|
||||
color: _chartColors[i],
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<div class="chart-holder">
|
||||
<app-mempool-graph
|
||||
[template]="'advanced'"
|
||||
[limitFee]="500"
|
||||
[height]="600"
|
||||
[left]="60"
|
||||
[right]="10"
|
||||
|
||||
@@ -11,6 +11,15 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
interval: number;
|
||||
text: string;
|
||||
units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
precisionThresholds = {
|
||||
year: 100,
|
||||
month: 18,
|
||||
week: 12,
|
||||
day: 31,
|
||||
hour: 48,
|
||||
minute: 90,
|
||||
second: 90
|
||||
};
|
||||
intervals = {};
|
||||
|
||||
@Input() time: number;
|
||||
@@ -85,8 +94,12 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
let counter: number;
|
||||
for (const [index, unit] of this.units.entries()) {
|
||||
const precisionUnit = this.units[Math.min(this.units.length - 1), index + this.precision];
|
||||
let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)];
|
||||
counter = Math.floor(seconds / this.intervals[unit]);
|
||||
const precisionCounter = Math.floor(seconds / this.intervals[precisionUnit]);
|
||||
if (precisionCounter > this.precisionThresholds[precisionUnit]) {
|
||||
precisionUnit = unit;
|
||||
}
|
||||
if (counter > 0) {
|
||||
let rounded = Math.round(seconds / this.intervals[precisionUnit]);
|
||||
if (this.fractionDigits) {
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo™, the mempool.space logos™, the mempool square logo™, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
|
||||
<li>What to Do When You See Abuse</li>
|
||||
|
||||
|
||||
@@ -18,19 +18,7 @@
|
||||
</span>
|
||||
|
||||
<div class="container-buttons">
|
||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
||||
<button *ngIf="latestBlock" type="button" class="btn btn-sm btn-success">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="tx && !tx?.status?.confirmed && replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="tx && !tx?.status?.confirmed && !replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" [replaced]="replaced"></app-confirmations>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -271,6 +259,10 @@
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
|
||||
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</td>
|
||||
<td [innerHTML]="'‎' + (cpfpInfo.adjustedVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
||||
@@ -289,6 +281,10 @@
|
||||
<td i18n="transaction.locktime">Locktime</td>
|
||||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="cpfpInfo && cpfpInfo.adjustedVsize && cpfpInfo.adjustedVsize > (tx.weight / 4)">
|
||||
<td i18n="transaction.sigops|Transaction Sigops">Sigops</td>
|
||||
<td [innerHTML]="'‎' + (cpfpInfo.sigops | number)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.hex">Transaction hex</td>
|
||||
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
@@ -477,12 +473,13 @@
|
||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
||||
|
||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
|
||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
||||
<td *ngIf="cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td *ngIf="!cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
@@ -490,7 +487,7 @@
|
||||
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -86,6 +86,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
segwitEnabled: boolean;
|
||||
rbfEnabled: boolean;
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
|
||||
@ViewChild('graphContainer')
|
||||
graphContainer: ElementRef;
|
||||
@@ -157,6 +158,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
.subscribe((cpfpInfo) => {
|
||||
if (!cpfpInfo || !this.tx) {
|
||||
this.cpfpInfo = null;
|
||||
this.hasEffectiveFeeRate = false;
|
||||
return;
|
||||
}
|
||||
// merge ancestors/descendants
|
||||
@@ -164,16 +166,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||
relatives.push(cpfpInfo.bestDescendant);
|
||||
}
|
||||
let totalWeight =
|
||||
this.tx.weight +
|
||||
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
let totalFees =
|
||||
this.tx.fee +
|
||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
|
||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||
const hasRelatives = !!relatives.length;
|
||||
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
|
||||
let totalWeight =
|
||||
this.tx.weight +
|
||||
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
let totalFees =
|
||||
this.tx.fee +
|
||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||
} else {
|
||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
}
|
||||
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
});
|
||||
|
||||
this.fetchRbfSubscription = this.fetchRbfHistory$
|
||||
@@ -359,6 +369,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant,
|
||||
};
|
||||
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
@@ -382,7 +394,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
||||
this.latestBlock = block;
|
||||
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
@@ -500,6 +512,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.replaced = false;
|
||||
this.transactionTime = -1;
|
||||
this.cpfpInfo = null;
|
||||
this.hasEffectiveFeeRate = false;
|
||||
this.rbfInfo = null;
|
||||
this.rbfReplaces = [];
|
||||
this.showCpfpDetails = false;
|
||||
|
||||
@@ -114,7 +114,12 @@
|
||||
<ng-container *ngFor="let witness of vin.witness; index as i">
|
||||
<input type="checkbox" [id]="'tx' + vindex + 'witness' + i" style="display: none;">
|
||||
<p class="witness-item" [class.accordioned]="witness.length > 1000">
|
||||
{{ witness }}
|
||||
<ng-template [ngIf]="witness" [ngIfElse]="emptyWitnessItem">
|
||||
{{ witness }}
|
||||
</ng-template>
|
||||
<ng-template #emptyWitnessItem>
|
||||
<empty>
|
||||
</ng-template>
|
||||
</p>
|
||||
<div class="witness-toggle" *ngIf="witness.length > 1000">
|
||||
<span class="ellipsis">...</span>
|
||||
@@ -293,14 +298,7 @@
|
||||
|
||||
<div class="float-right">
|
||||
<ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-sm btn-success mt-2">
|
||||
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
<ng-template #unconfirmedButton>
|
||||
<button type="button" class="btn btn-sm btn-danger mt-2" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx?.status?.block_height" buttonClass="mt-2"></app-confirmations>
|
||||
</ng-container>
|
||||
<button *ngIf="address === ''; else viewingAddress" type="button" class="btn btn-sm btn-primary mt-2 ml-2" (click)="switchCurrency()">
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
<div class="mempool-graph">
|
||||
<app-mempool-graph
|
||||
[template]="'widget'"
|
||||
[limitFee]="150"
|
||||
[limitFilterFee]="1"
|
||||
[data]="mempoolStats.value?.mempool"
|
||||
[windowPreferenceOverride]="'2h'"
|
||||
></app-mempool-graph>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DashboardComponent } from '../dashboard/dashboard.component';
|
||||
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
|
||||
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
|
||||
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
|
||||
import { BlockPredictionGraphComponent } from '../components/block-prediction-graph/block-prediction-graph.component';
|
||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@NgModule({
|
||||
@@ -46,7 +46,7 @@ import { CommonModule } from '@angular/common';
|
||||
LbtcPegsGraphComponent,
|
||||
HashrateChartComponent,
|
||||
HashrateChartPoolsComponent,
|
||||
BlockPredictionGraphComponent,
|
||||
BlockHealthGraphComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { BlockPredictionGraphComponent } from '../components/block-prediction-graph/block-prediction-graph.component';
|
||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||
import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component';
|
||||
import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component';
|
||||
import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component';
|
||||
@@ -143,9 +143,9 @@ const routes: Routes = [
|
||||
redirectTo: 'mempool',
|
||||
},
|
||||
{
|
||||
path: 'mining/block-prediction',
|
||||
path: 'mining/block-health',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockPredictionGraphComponent,
|
||||
component: BlockHealthGraphComponent,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Transaction {
|
||||
ancestors?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: number;
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
||||
@@ -24,6 +24,10 @@ export interface CpfpInfo {
|
||||
ancestors: Ancestor[];
|
||||
descendants?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
effectiveFeePerVsize?: number;
|
||||
sigops?: number;
|
||||
adjustedVsize?: number;
|
||||
acceleration?: number;
|
||||
}
|
||||
|
||||
export interface RbfInfo {
|
||||
@@ -155,7 +159,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
}
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
@@ -165,6 +169,7 @@ interface RbfTransaction extends TransactionStripped {
|
||||
export interface MempoolPosition {
|
||||
block: number,
|
||||
vsize: number,
|
||||
accelerated?: boolean
|
||||
}
|
||||
|
||||
export interface RewardStats {
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface WebsocketResponse {
|
||||
block?: BlockExtended;
|
||||
blocks?: BlockExtended[];
|
||||
conversions?: any;
|
||||
txConfirmed?: boolean;
|
||||
txConfirmed?: string;
|
||||
historicalDate?: string;
|
||||
mempoolInfo?: MempoolInfo;
|
||||
vBytesPerSecond?: number;
|
||||
@@ -57,7 +57,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
export interface MempoolBlockDelta {
|
||||
added: TransactionStripped[],
|
||||
removed: string[],
|
||||
changed?: { txid: string, rate: number | undefined }[];
|
||||
changed?: { txid: string, rate: number | undefined, acc: number | undefined }[];
|
||||
}
|
||||
|
||||
export interface MempoolInfo {
|
||||
@@ -75,8 +75,9 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
acc?: number; // acceleration delta
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -253,20 +253,4 @@ export class NodeFeeChartComponent implements OnInit {
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
}), `node-fee-chart.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,21 +252,4 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
const now = new Date();
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
}), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
excludeComponents: ['dataZoom'],
|
||||
}), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
}), `lightning-nodes-per-network-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
|
||||
@@ -238,7 +238,7 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalBlockPrediction$(interval: string | undefined) : Observable<any> {
|
||||
getHistoricalBlocksHealth$(interval: string | undefined) : Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/predictions` +
|
||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||
|
||||
@@ -92,7 +92,7 @@ export class StateService {
|
||||
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
lightningChanged$ = new ReplaySubject<boolean>(1);
|
||||
blocks$: ReplaySubject<[BlockExtended, boolean]>;
|
||||
blocks$: ReplaySubject<[BlockExtended, string]>;
|
||||
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||
conversions$ = new ReplaySubject<any>(1);
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
@@ -163,7 +163,7 @@ export class StateService {
|
||||
}
|
||||
});
|
||||
|
||||
this.blocks$ = new ReplaySubject<[BlockExtended, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
|
||||
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
if (this.env.BASE_MODULE === 'bisq') {
|
||||
this.network = this.env.BASE_MODULE;
|
||||
|
||||
@@ -241,7 +241,7 @@ export class WebsocketService {
|
||||
blocks.forEach((block: BlockExtended) => {
|
||||
if (block.height > this.stateService.latestBlockHeight) {
|
||||
maxHeight = Math.max(maxHeight, block.height);
|
||||
this.stateService.blocks$.next([block, false]);
|
||||
this.stateService.blocks$.next([block, '']);
|
||||
}
|
||||
});
|
||||
this.stateService.updateChainTip(maxHeight);
|
||||
@@ -258,7 +258,7 @@ export class WebsocketService {
|
||||
if (response.block) {
|
||||
if (response.block.height > this.stateService.latestBlockHeight) {
|
||||
this.stateService.updateChainTip(response.block.height);
|
||||
this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
|
||||
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
||||
}
|
||||
|
||||
if (response.txConfirmed) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<ng-template [ngIf]="confirmations">
|
||||
<button type="button" class="btn btn-sm btn-success {{buttonClass}}">
|
||||
<ng-container *ngTemplateOutlet="confirmations == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: confirmations}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmations',
|
||||
templateUrl: './confirmations.component.html',
|
||||
styleUrls: ['./confirmations.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ConfirmationsComponent implements OnChanges {
|
||||
@Input() chainTip: number;
|
||||
@Input() height: number;
|
||||
@Input() replaced: boolean = false;
|
||||
@Input() hideUnconfirmed: boolean = false;
|
||||
@Input() buttonClass: string = '';
|
||||
|
||||
confirmations: number = 0;
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.chainTip != null && this.height != null) {
|
||||
this.confirmations = Math.max(1, this.chainTip - this.height + 1);
|
||||
} else {
|
||||
this.confirmations = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,85 @@
|
||||
<footer *ngIf="networkPaths$ | async as networkPaths">
|
||||
<div class="pref-selectors">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row main">
|
||||
<div class="offset-lg-1 col-lg-4 col align-self-center branding">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<p><ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></p>
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
<ng-template #temporaryHidden>
|
||||
<a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a>
|
||||
<p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p>
|
||||
</ng-template>
|
||||
<p class="cta-secondary"><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace && env.LIGHTNING" class="cta-secondary"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/about' | relativeUrl]">About The Mempool Open Source Project™</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
<p><a [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Learn</p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool">What is a mempool?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer">What is a block explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer">What is a mempool explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs ›</a></p>
|
||||
</div>
|
||||
<!--<div class="col-lg-4 links">
|
||||
<p class="category">Connect</p>
|
||||
<p><a href="https://github.com/mempool" target="_blank">GitHub</a></p>
|
||||
<p><a href="https://twitter.com/mempool" target="_blank">Twitter</a></p>
|
||||
<p><a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank">Nostr</a></p>
|
||||
<p><a href="https://youtube.com/@mempool" target="_blank">YouTube</a></p>
|
||||
<p><a href="https://bitcointv.com/c/mempool/videos" target="_blank">BitcoinTV</a></p>
|
||||
<p><a href="https://mempool.chat" target="_blank">Matrix</a></p>
|
||||
</div>-->
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">More Networks</p>
|
||||
<p *ngIf="currentNetwork !== '' && currentNetwork !== 'mainnet'"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'testnet'"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'signet'"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'liquid' && currentNetwork !== 'liquidtestnet'"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'bisq'"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Legal</p>
|
||||
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
|
||||
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
|
||||
<p><a [routerLink]="['/trademark-policy']">Trademark Policy</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
<div class="row social-links">
|
||||
<div class="col-sm-12">
|
||||
<a href="https://github.com/mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
|
||||
<a href="https://twitter.com/mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a>
|
||||
<a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank"><svg fill="#fff" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 875 875"><path d="M684.72 485.57c.22 12.59-11.93 51.47-38.67 81.3-26.74 29.83-56.02 20.85-58.42 20.16s-3.09-4.46-7.89-3.77-9.6 6.17-18.86 7.2-17.49 1.71-26.06-1.37c-4.46.69-5.14.71-7.2 2.24s-17.83 10.79-21.6 11.47c0 7.2-1.37 44.57 0 55.89s3.77 25.71 7.54 36 2.74 10.63 7.54 9.94 13.37.34 15.77 4.11c2.4 3.77 1.37 6.51 5.49 8.23s60.69 17.14 99.43 19.2c26.74.69 42.86 2.74 52.12 19.54 1.37 7.89 7.54 13.03 11.31 14.06s8.23 2.06 12 5.83 1.03 8.23 5.49 11.66c4.46 3.43 14.74 8.57 25.37 13.71 10.63 5.14 15.09 13.37 15.77 16.11s1.71 10.97 1.71 10.97-8.91 0-10.97-2.06-2.74-5.83-2.74-5.83-6.17 1.03-7.54 3.43.69 2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8 4.11 8.23 5.83 8.23 10.63 10.97s8.23 5.83 8.23 5.83l-7.2 4.46s-4.46 2.06-14.74-.69-11.66-4.46-12.69-10.63 0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77 4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97 0-19.2 3.43-19.89 1.71-26.74-14.06-55.89-19.89-64.12c-13.03 1.03-50.74-.69-50.74-.69s-2.4-.69-17.49 5.83-36.48 13.76-46.77 19.93-14.4 9.7-16.12 13.13c.12 3-1.23 7.72-2.79 9.06s-12.48 2.42-12.48 2.42-5.85 5.86-8.25 9.97c-6.86 9.6-55.2 125.14-66.52 149.83-13.54 32.57-9.77 27.43-37.71 27.43s-8.06.3-8.06.3-12.34 5.88-16.8 5.88-18.86-2.4-26.4 0-16.46 9.26-23.31 10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12s1.74-6.51 7.91-10.88c8.23-5.83 25.37-16.11 34.63-21.26s17.49-7.89 23.31-9.26 18.51-6.17 30.51-9.94 19.54-8.23 29.83-31.54 50.4-111.43 51.43-116.23c.63-2.96 3.73-6.48 4.8-15.09.66-5.35-2.49-13.04 1.71-22.63 10.97-25.03 21.6-20.23 26.4-20.23s17.14.34 26.4-1.37 15.43-2.74 24.69-7.89 11.31-8.91 11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2 1.03 8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17 13.03-1.71-18.86-5.14 7.2-2.74-16.11-4.8 8.23-3.43-14.4-5.83 4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46 3.43-4.46 3.43l1.37 12-12.2-6.27-7-11.9s2.36 4.01-9.62 7.53c-20.55 0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89 3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26 25.71-22.97 25.03-16.46 46.29-17.14c21.26-.69 32.91 2.74 46.29 8.23s38.74 13.71 43.89 17.49c11.31-9.94 28.46-19.89 34.29-19.89 1.03-2.4 6.19-12.33 17.96-17.6 35.31-15.81 108.13-34 131.53-35.54 31.2-2.06 7.89-1.37 39.09 2.06 31.2 3.43 54.17 7.54 69.6 12.69 12.58 4.19 25.03 9.6 34.29 2.06 4.33-1.81 11.81-1.34 17.83-5.14 30.69-25.09 34.72-32.35 43.63-41.95s20.14-24.91 22.54-45.14 4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03 2.06-12 5.14-22.29 6.86-30.51s9.94-14.74 19.89-16.46c9.94-1.71 17.83 1.37 22.29 4.8 4.46 3.43 11.65 6.28 13.37 10.29.34 1.71-1.37 6.51 8.23 8.23 9.6 1.71 16.05 4.16 16.05 4.16s15.64 4.29 3.11 7.73c-12.69 2.06-20.52-.71-24.29 1.69s-7.21 10.08-9.61 11.1-7.2.34-12 4.11-9.6 6.86-12.69 14.4-5.49 15.77-3.43 26.74 8.57 31.54 14.4 43.2c5.83 11.66 20.23 40.8 24.34 47.66s15.77 29.49 16.8 53.83 1.03 44.23 0 54.86-10.84 51.65-35.53 85.94c-8.16 14.14-23.21 31.9-24.67 35.03-1.45 3.13-3.02 4.88-1.61 7.65 4.62 9.05 12.87 22.13 14.71 29.22 2.29 6.64 6.99 16.13 7.22 28.72Z" style="stroke:#000;stroke-miterlimit:10;stroke-width:6px"/></svg></a>
|
||||
<a href="https://youtube.com/@mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg></a>
|
||||
<a href="https://bitcointv.com/c/mempool/videos" target="_blank"><svg _ngcontent-serverApp-c90="" xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path _ngcontent-serverApp-c90="" d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="#fff"></path><path _ngcontent-serverApp-c90="" d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path _ngcontent-serverApp-c90="" d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="#fff"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse _ngcontent-serverApp-c90="" fill="#fff" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path _ngcontent-serverApp-c90="" fill="#fff" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse _ngcontent-serverApp-c90="" fill="#fff" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path _ngcontent-serverApp-c90="" fill="#fff" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path _ngcontent-serverApp-c90="" d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path _ngcontent-serverApp-c90="" d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg></a>
|
||||
<a href="https://mempool.chat" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row version">
|
||||
<div class="col-sm-12">
|
||||
<p *ngIf="officialMempoolSpace">{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a target="_blank" href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]</p>
|
||||
|
||||
<div class="terms-of-service">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
|
|
||||
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
|
||||
|
|
||||
<a *ngIf="officialMempoolSpace && networkPaths['mainnet'] === '/lightning' else broadcastTransaction" [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
|
||||
<ng-template #broadcastTransaction>
|
||||
<a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
|
||||
</ng-template>
|
||||
<p *ngIf="!officialMempoolSpace">v{{ packetJsonVersion }} [<a target="_blank" href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
@@ -1,22 +1,106 @@
|
||||
footer {
|
||||
background-color: #1d1f31;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
footer .row.main {
|
||||
padding: 40px 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer .row.main .branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
footer .row.main .branding .btn {
|
||||
display: inline-block;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
footer .row.main .branding button.account {
|
||||
background-color: #2d3348;
|
||||
}
|
||||
|
||||
footer .row.main .branding .cta {
|
||||
margin: 20px auto 25px auto;
|
||||
}
|
||||
|
||||
footer .row.main .branding .cta-secondary {
|
||||
|
||||
}
|
||||
|
||||
footer .row.main .links.outer {
|
||||
padding-left: 24px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
footer .row.main .links > div:first-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
footer .row.main .links .category {
|
||||
color: #4a68b9;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
footer .row.main .links .category:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pref-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.selector {
|
||||
margin-left: .5em;
|
||||
margin-bottom: .5em;
|
||||
&:first {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
footer .selector {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
footer .row.social-links {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
footer .row.social-links a {
|
||||
margin: 0 14px;
|
||||
}
|
||||
|
||||
footer .row.social-links svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
footer .row.version {
|
||||
padding: 20px !important;
|
||||
background-color: #11131f;
|
||||
}
|
||||
|
||||
footer .row.version p {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
footer .row.version p a {
|
||||
color: #09a3ba;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
footer .row.main .links.outer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .row.main .links.outer > .row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
footer .row.main .links.outer > .row > div:first-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { Observable, merge, of, Subject } from 'rxjs';
|
||||
import { tap, takeUntil } from 'rxjs/operators';
|
||||
import { Env, StateService } from '../../../services/state.service';
|
||||
import { IBackendInfo } from '../../../interfaces/websocket.interface';
|
||||
import { LanguageService } from '../../../services/language.service';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-footer',
|
||||
@@ -10,19 +13,57 @@ import { Env, StateService } from '../../../services/state.service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class GlobalFooterComponent implements OnInit {
|
||||
private destroy$: Subject<any> = new Subject<any>();
|
||||
env: Env;
|
||||
networkPaths: { [network: string]: string };
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
networkPaths$: Observable<Record<string, string>>;
|
||||
backendInfo$: Observable<IBackendInfo>;
|
||||
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
|
||||
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
|
||||
urlLanguage: string;
|
||||
network$: Observable<string>;
|
||||
networkPaths: { [network: string]: string };
|
||||
currentNetwork = '';
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private navigationService: NavigationService,
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.networkPaths$ = this.navigationService.subnetPaths;
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$).pipe(
|
||||
tap((network: string) => {
|
||||
return network;
|
||||
})
|
||||
);
|
||||
this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => {
|
||||
this.currentNetwork = network;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(true);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
networkLink(network) {
|
||||
const thisNetwork = network || 'mainnet';
|
||||
if( network === '' || network === 'mainnet' || network === 'testnet' || network === 'signet' ) {
|
||||
return (this.env.BASE_MODULE === 'mempool' ? '' : this.env.MEMPOOL_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
|
||||
}
|
||||
if( network === 'liquid' || network === 'liquidtestnet' ) {
|
||||
return (this.env.BASE_MODULE === 'liquid' ? '' : this.env.LIQUID_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
|
||||
}
|
||||
if( network === 'bisq' ) {
|
||||
return (this.env.BASE_MODULE === 'bisq' ? '' : this.env.BISQ_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ import { SatsComponent } from './components/sats/sats.component';
|
||||
import { TruncateComponent } from './components/truncate/truncate.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
import { ConfirmationsComponent } from './components/confirmations/confirmations.component';
|
||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
||||
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
|
||||
@@ -175,6 +176,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ConfirmationsComponent,
|
||||
ToggleComponent,
|
||||
GeolocationComponent,
|
||||
TestnetAlertComponent,
|
||||
@@ -289,6 +291,7 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ConfirmationsComponent,
|
||||
ToggleComponent,
|
||||
GeolocationComponent,
|
||||
PreviewTitleComponent,
|
||||
|
||||
@@ -62,6 +62,10 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
@media (max-width: 767.98px) {
|
||||
min-height: 100vh;
|
||||
@@ -1143,7 +1147,7 @@ th {
|
||||
display: none;
|
||||
}
|
||||
|
||||
app-master-page, app-liquid-master-page {
|
||||
app-master-page, app-liquid-master-page, app-bisq-master-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
@@ -1155,4 +1159,4 @@ app-master-page, app-liquid-master-page {
|
||||
|
||||
app-global-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,10 +356,10 @@ ELEMENTS_REPO_BRANCH=master
|
||||
ELEMENTS_LATEST_RELEASE=elements-22.1
|
||||
echo -n '.'
|
||||
|
||||
BITCOIN_ELECTRS_REPO_URL=https://github.com/blockstream/electrs
|
||||
BITCOIN_ELECTRS_REPO_URL=https://github.com/mempool/electrs
|
||||
BITCOIN_ELECTRS_REPO_NAME=electrs
|
||||
BITCOIN_ELECTRS_REPO_BRANCH=new-index
|
||||
BITCOIN_ELECTRS_LATEST_RELEASE=new-index
|
||||
BITCOIN_ELECTRS_REPO_BRANCH=mempool
|
||||
BITCOIN_ELECTRS_LATEST_RELEASE=mempool
|
||||
|
||||
ELEMENTS_ELECTRS_REPO_URL=https://github.com/blockstream/electrs
|
||||
ELEMENTS_ELECTRS_REPO_NAME=electrs
|
||||
@@ -1312,8 +1312,13 @@ case $OS in
|
||||
osSudo "${ROOT_USER}" pw usermod ${MEMPOOL_USER} -G "${CLN_GROUP}"
|
||||
osSudo "${ROOT_USER}" chsh -s `which zsh` "${CLN_USER}"
|
||||
echo "export PATH=$PATH:$HOME/.local/bin" >> "${CLN_HOME}/.zshrc"
|
||||
osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}"
|
||||
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning" "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}"
|
||||
osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/bitcoin"
|
||||
osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/signet"
|
||||
osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/testnet"
|
||||
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning"
|
||||
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning/bitcoin"
|
||||
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning/signet"
|
||||
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning/testnet"
|
||||
osSudo "${ROOT_USER}" chown -R "${CLN_USER}:${CLN_GROUP}" "${CLN_HOME}"
|
||||
echo "[*] Creating symlink to .bitcoin folder"
|
||||
osSudo "${CLN_USER}" ln -s "${BITCOIN_HOME}/.bitcoin" "${CLN_HOME}/.bitcoin"
|
||||
@@ -1321,16 +1326,18 @@ case $OS in
|
||||
echo "[*] Installing Core Lightning package"
|
||||
osPackageInstall ${CLN_PKG}
|
||||
|
||||
echo "[*] Installing Core Lightning mainnet Cronjob"
|
||||
public_ipv4=$( ifconfig | grep 'inet ' | awk -F ' ' '{ print $2 }' | grep -v '^103\.165\.192\.' | grep -v '^127\.0\.0\.1' )
|
||||
public_ipv6=$( ifconfig | grep 'inet6' | awk -F ' ' '{ print $2 }' | grep -v '^2001:df6:7280::' | grep -v '^fe80::' | grep -v '^::1' )
|
||||
|
||||
crontab_cln+="@reboot sleep 10 ; screen -dmS main lightningd --rpc-file-mode 0660 --alias `hostname` --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
|
||||
crontab_cln+="@reboot sleep 10 ; screen -dmS tes lightningd --rpc-file-mode 0660 --alias `hostname` --network testnet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
|
||||
crontab_cln+="@reboot sleep 10 ; screen -dmS sig lightningd --rpc-file-mode 0660 --alias `hostname` --network signet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6 \n"
|
||||
crontab_cln+="@reboot sleep 20 ; /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
|
||||
crontab_cln+="1 * * * * /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
|
||||
echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
|
||||
######## FIXME: this code doesn't work properly, needs fixing
|
||||
#
|
||||
# echo "[*] Installing Core Lightning mainnet Cronjob"
|
||||
# public_ipv4=$( ifconfig | grep 'inet ' | awk -F ' ' '{ print $2 }' | grep -v '^103\.165\.192\.' | grep -v '^127\.0\.0\.1' )
|
||||
# public_ipv6=$( ifconfig | grep 'inet6' | awk -F ' ' '{ print $2 }' | grep -v '^2001:df6:7280::' | grep -v '^fe80::' | grep -v '^::1' )
|
||||
#
|
||||
# crontab_cln+="@reboot sleep 10 ; screen -dmS main lightningd --rpc-file-mode 0660 --alias `hostname` --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
|
||||
# crontab_cln+="@reboot sleep 10 ; screen -dmS tes lightningd --rpc-file-mode 0660 --alias `hostname` --network testnet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
|
||||
# crontab_cln+="@reboot sleep 10 ; screen -dmS sig lightningd --rpc-file-mode 0660 --alias `hostname` --network signet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6 \n"
|
||||
# crontab_cln+="@reboot sleep 20 ; /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
|
||||
# crontab_cln+="1 * * * * /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
|
||||
# echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
|
||||
;;
|
||||
Debian)
|
||||
;;
|
||||
@@ -1813,7 +1820,7 @@ echo "[*] Adding MySQL configuration"
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
osSudo "${ROOT_USER}" service mysql-server start
|
||||
osSudo "${ROOT_USER}" service mysql-server onestart
|
||||
;;
|
||||
Debian)
|
||||
osSudo "${ROOT_USER}" service mysql start
|
||||
|
||||
@@ -59,7 +59,7 @@ location = / {
|
||||
}
|
||||
|
||||
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
|
||||
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
|
||||
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css))$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
}
|
||||
@@ -92,7 +92,7 @@ location /resources/config. {
|
||||
}
|
||||
|
||||
# cache /main.f40e91d908a068a2.js forever since they never change
|
||||
location ~* ^/.+\..+\.(js|css) {
|
||||
location ~* ^/.+\..+\.(js|css)$ {
|
||||
try_files /$lang/$uri /en-US/$uri =404;
|
||||
expires 1y;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user