Compare commits

..

1 Commits

Author SHA1 Message Date
Mononaut
b8d24e8cba Activation height logic for standardness rule changes 2024-08-30 23:12:24 +00:00
84 changed files with 969 additions and 2075 deletions

View File

@@ -13,10 +13,10 @@
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"redis": "^4.7.0",
@@ -2278,10 +2278,9 @@
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -2490,9 +2489,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -2502,7 +2501,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -3031,9 +3030,9 @@
"dev": true
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
@@ -3461,36 +3460,36 @@
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -3603,12 +3602,12 @@
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -6052,12 +6051,9 @@
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/merge-stream": {
"version": "2.0.0",
@@ -6271,12 +6267,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6444,9 +6437,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -6654,11 +6647,11 @@
]
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.6"
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
@@ -6879,9 +6872,9 @@
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -6914,14 +6907,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6933,14 +6918,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -9454,9 +9439,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
},
"axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -9619,9 +9604,9 @@
}
},
"body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -9631,7 +9616,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -10012,9 +9997,9 @@
"dev": true
},
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"error-ex": {
"version": "1.3.2",
@@ -10319,36 +10304,36 @@
}
},
"express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -10450,12 +10435,12 @@
}
},
"finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -12252,9 +12237,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"merge-stream": {
"version": "2.0.0",
@@ -12417,9 +12402,9 @@
}
},
"object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
},
"on-finished": {
"version": "2.4.1",
@@ -12536,9 +12521,9 @@
"dev": true
},
"path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"path-type": {
"version": "4.0.0",
@@ -12680,11 +12665,11 @@
"dev": true
},
"qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.6"
"side-channel": "^1.0.4"
}
},
"queue-microtask": {
@@ -12818,9 +12803,9 @@
"dev": true
},
"send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -12852,11 +12837,6 @@
}
}
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12870,14 +12850,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
"send": "0.18.0"
}
},
"set-function-length": {

View File

@@ -42,10 +42,10 @@
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt",

View File

@@ -1,5 +1,5 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
@@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
test('should detect RBF transactions with fast method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
test('should detect RBF transactions with scalable method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

@@ -20,7 +20,6 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
class BitcoinRoutes {
public initRoutes(app: Application) {
@@ -87,7 +86,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -106,13 +105,13 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 500, 'Not an array');
res.status(500).send('Not an array');
return;
}
const txIds: string[] = [];
@@ -129,12 +128,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
handleError(req, res, 500, 'Invalid txids format');
res.status(500).send('Invalid txids format');
return;
}
const txids = txids_csv.split(',');
if (txids.length > 50) {
handleError(req, res, 400, 'Too many txids requested');
res.status(400).send('Too many txids requested');
return;
}
@@ -142,13 +141,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID.`);
res.status(501).send(`Invalid transaction ID.`);
return;
}
@@ -181,7 +180,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
handleError(req, res, 500, 'failed to get CPFP info');
res.status(500).send('failed to get CPFP info');
return;
}
}
@@ -210,7 +209,7 @@ class BitcoinRoutes {
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
@@ -224,7 +223,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
@@ -285,13 +284,13 @@ class BitcoinRoutes {
// Not modified
// 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
}
} catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@@ -305,7 +304,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
@@ -315,7 +314,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -337,7 +336,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -347,7 +346,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -358,11 +357,10 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `audit not available`);
return;
return res.status(404).send(`audit not available`);
}
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -373,8 +371,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `transaction audit not available`);
return;
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@@ -391,49 +388,42 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
}
if (!Common.indexingEnabled()) {
handleError(req, res, 404, `Indexing is required for this API`);
return;
return res.status(404).send(`Indexing is required for this API`);
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
}
if (from > to) {
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
}
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -468,10 +458,10 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -493,7 +483,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -502,13 +492,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -517,16 +507,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
return res.status(413).send(e instanceof Error ? e.message : e);
}
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -539,23 +528,23 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e);
res.status(413).send(e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
res.status(405).send('Address summary lookups require mempool/electrs backend.');
return;
}
}
private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -566,16 +555,15 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
return res.status(413).send(e instanceof Error ? e.message : e);
}
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -590,16 +578,16 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e);
res.status(413).send(e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
return;
}
}
@@ -609,7 +597,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -636,7 +624,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -644,13 +632,12 @@ class BitcoinRoutes {
try {
const result = blocks.getCurrentBlockHeight();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
return res.status(503).send(`Service Temporarily Unavailable`);
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -660,7 +647,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -670,7 +657,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -679,7 +666,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -688,7 +675,7 @@ class BitcoinRoutes {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -701,7 +688,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -710,7 +697,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -719,7 +706,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -732,7 +719,7 @@ class BitcoinRoutes {
res.status(204).send();
}
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -741,7 +728,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -751,10 +738,10 @@ class BitcoinRoutes {
if (da) {
res.json(da);
} else {
handleError(req, res, 503, `Service Temporarily Unavailable`);
res.status(503).send(`Service Temporarily Unavailable`);
}
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -765,7 +752,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -777,7 +764,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -789,7 +776,8 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}

View File

@@ -10,6 +10,7 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -79,8 +80,8 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@@ -95,7 +96,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
matches[addedTx.txid] = [...new Set(foundMatches)];
}
});
} else {
@@ -123,7 +124,7 @@ export class Common {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
matches[addedTx.txid] = [...foundMatches];
}
}
}
@@ -138,17 +139,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const key = `${vin.txid}:${vin.vout}`;
const match = spendMap.get(key);
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
if (match && match.txid !== tx.txid) {
replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey);
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
}
}
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
}
if (replaced.size) {

View File

@@ -257,7 +257,6 @@ class DiskCache {
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}
} catch (e) {

View File

@@ -1,7 +1,6 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
class ChannelsRoutes {
constructor() { }
@@ -23,7 +22,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -39,7 +38,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -54,11 +53,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) {
handleError(req, res, 400, 'Invalid index');
res.status(400).send('Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
handleError(req, res, 400, 'Invalid status');
res.status(400).send('Invalid status');
return;
}
@@ -70,14 +69,14 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try {
if (!Array.isArray(req.query.txId)) {
handleError(req, res, 400, 'Not an array');
res.status(400).send('Not an array');
return;
}
const txIds: string[] = [];
@@ -108,7 +107,7 @@ class ChannelsRoutes {
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -120,7 +119,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -133,7 +132,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}

View File

@@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import channelsApi from './channels.api';
import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes {
constructor() { }
@@ -29,7 +27,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -43,7 +41,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -52,7 +50,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes {
constructor() { }
@@ -32,7 +31,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -182,13 +181,13 @@ class NodesRoutes {
}
} catch (e) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -196,7 +195,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@@ -204,7 +203,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -216,7 +215,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -224,7 +223,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) {
handleError(req, res, 404, 'Node not found');
res.status(404).send('Node not found');
return;
}
res.header('Pragma', 'public');
@@ -232,7 +231,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -248,7 +247,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes,
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -260,7 +259,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -272,7 +271,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -284,7 +283,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -296,7 +295,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -308,7 +307,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -323,7 +322,7 @@ class NodesRoutes {
);
if (country.length === 0) {
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -336,7 +335,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -350,7 +349,7 @@ class NodesRoutes {
);
if (isp.length === 0) {
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -363,7 +362,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -375,7 +374,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@@ -3,7 +3,6 @@ import { Application, Request, Response } from 'express';
import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes {
public initRoutes(app: Application) {
@@ -43,7 +42,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length);
res.send(result);
} else {
handleError(req, res, 404, 'Asset icon not found');
res.status(404).send('Asset icon not found');
}
}
@@ -52,7 +51,7 @@ class LiquidRoutes {
if (result) {
res.json(result);
} else {
handleError(req, res, 404, 'Asset icons not found');
res.status(404).send('Asset icons not found');
}
}
@@ -83,7 +82,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -95,7 +94,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -107,7 +106,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -119,7 +118,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -131,7 +130,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -143,7 +142,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -155,7 +154,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -167,7 +166,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -179,7 +178,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -191,7 +190,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -203,7 +202,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -215,7 +214,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -227,7 +226,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -239,7 +238,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -251,7 +250,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}

View File

@@ -369,7 +369,7 @@ class MempoolBlocks {
const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8;
let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7];
@@ -380,36 +380,28 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
}
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
if (mempoolTx) {
// ugly micro-optimization to avoid allocating new arrays
ancestors.length = 0;
descendants.length = 0;
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) {
matched = true;
} else {
if (!ancestor) {
if (!mempool[txid]) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
}
const relative = {
txid: txid,
fee: ancestor.fee,
weight: (ancestor.adjustedVsize * 4),
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
mempoolTx.lastBoosted = ancestor.firstSeen;
}
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
} else {
ancestors.push(relative);
}
@@ -418,20 +410,7 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
// ugly micro-optimization to avoid allocating new arrays or objects
if (mempoolTx.ancestors) {
mempoolTx.ancestors.length = 0;
} else {
mempoolTx.ancestors = [];
}
if (mempoolTx.descendants) {
mempoolTx.descendants.length = 0;
} else {
mempoolTx.descendants = [];
}
mempoolTx.ancestors.push(...ancestors);
mempoolTx.descendants.push(...descendants);
mempoolTx.cpfpChecked = true;
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
}
@@ -441,10 +420,7 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
let acceleration: Acceleration;
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
@@ -460,8 +436,7 @@ class MempoolBlocks {
}
}
for (let i = 0; i < block.length; i++) {
const txid = block[i];
for (const txid of block) {
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
@@ -470,37 +445,30 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2),
};
if (txid in accelerations) {
acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
const acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
@@ -518,7 +486,7 @@ class MempoolBlocks {
}
}
}
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
return this.dataToMempoolBlocks(
block,
transactions,
totalSize,
@@ -526,7 +494,7 @@ class MempoolBlocks {
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
);
};
});
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);

View File

@@ -19,13 +19,12 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
@@ -75,12 +74,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@@ -363,15 +362,12 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
this.recentlyDeleted.unshift(deletedTransactions);
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@@ -545,7 +541,16 @@ class Mempool {
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions

View File

@@ -10,7 +10,6 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -54,12 +53,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Prices are not available on testnets.');
res.status(400).send('Prices are not available on testnets.');
return;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@@ -72,7 +71,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -85,9 +84,9 @@ class MiningRoutes {
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@@ -104,9 +103,9 @@ class MiningRoutes {
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@@ -130,7 +129,7 @@ class MiningRoutes {
res.json(pools);
}
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -144,7 +143,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -158,7 +157,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -173,9 +172,9 @@ class MiningRoutes {
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
res.status(404).send(e.message);
} else {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
@@ -204,7 +203,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -218,7 +217,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -236,7 +235,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -250,7 +249,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -264,7 +263,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -282,7 +281,7 @@ class MiningRoutes {
weights: blockWeights
});
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -294,7 +293,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -318,7 +317,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -327,7 +326,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) {
handleError(req, res, 204, `This block has not been audited.`);
res.status(204).send(`This block has not been audited.`);
return;
}
@@ -336,7 +335,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -359,7 +358,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -372,7 +371,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -385,7 +384,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null');
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -395,12 +394,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -410,13 +409,13 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -426,12 +425,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -441,12 +440,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -456,12 +455,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(accelerationApi.accelerations || []);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -473,7 +472,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@@ -44,22 +44,6 @@ interface CacheEvent {
value?: any,
}
/**
* Singleton for tracking RBF trees
*
* Maintains a set of RBF trees, where each tree represents a sequence of
* consecutive RBF replacements.
*
* Trees are identified by the txid of the root transaction.
*
* To maintain consistency, the following invariants must be upheld:
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
* - Existence: X in treeMap => treeMap(X) in rbfTrees
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
*/
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@@ -77,10 +61,6 @@ class RbfCache {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@@ -112,12 +92,6 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
@@ -140,10 +114,6 @@ class RbfCache {
if (!replacedTx.rbf) {
txFullRbf = true;
}
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
@@ -170,47 +140,18 @@ class RbfCache {
}
}
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(newTree.tx.txid, newTree);
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
/**
* Read-only public interface
*/
public has(txId: string): boolean {
return this.txs.has(txId);
}
@@ -291,6 +232,32 @@ class RbfCache {
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
// is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
@@ -304,10 +271,6 @@ class RbfCache {
return tree?.fullRbf;
}
/**
* Cache maintenance & utility functions
*/
private cleanup(): void {
const now = Date.now();
for (const txid of this.expiring.keys()) {
@@ -336,6 +299,10 @@ class RbfCache {
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.removeTree(tx);
}
this.remove(tx);
}
}
@@ -403,21 +370,14 @@ class RbfCache {
};
}
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
public async load({ txs, trees, expiring, mempool }): Promise<void> {
try {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
if (tree) {
this.addTree(tree.tx.txid, tree);
this.updateTreeMap(tree.tx.txid, tree);
if (tree.mined) {
this.evict(tree.tx.txid);
}
}
for (const deflatedTree of trees) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
@@ -425,31 +385,6 @@ class RbfCache {
}
});
this.staleCount = 0;
// connect cached trees to current mempool transactions
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
for (const tree of this.rbfTrees.values()) {
const tx = this.getTx(tree.tx.txid);
if (!tx || tree.mined) {
continue;
}
for (const vin of tx.vin) {
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
if (conflict && conflict.txid !== tx.txid) {
if (!conflicts[conflict.txid]) {
conflicts[conflict.txid] = {
replacedBy: conflict,
replaces: new Set(),
};
}
conflicts[conflict.txid].replaces.add(tx);
}
}
}
for (const { replacedBy, replaces } of Object.values(conflicts)) {
this.add([...replaces.values()], replacedBy);
}
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
@@ -491,12 +426,6 @@ class RbfCache {
return;
}
// if this tx is already in the cache, return early
if (this.treeMap.has(txid)) {
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@@ -528,6 +457,10 @@ class RbfCache {
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree;
}
@@ -578,7 +511,6 @@ class RbfCache {
processTxs(txs);
}
// evict missing transactions
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);

View File

@@ -365,7 +365,6 @@ class RedisCache {
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations,
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}

View File

@@ -520,17 +520,8 @@ class WebsocketHandler {
}
}
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -538,8 +529,6 @@ class WebsocketHandler {
this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions;
let removed = deletedTransactions;
@@ -558,7 +547,7 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
@@ -589,7 +578,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid].replaced) {
for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
@@ -958,7 +947,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleRbfTransactions(rbfTransactions);
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {

View File

@@ -1,9 +0,0 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@@ -594,63 +594,4 @@ describe('Mainnet', () => {
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
describe('Accelerated Transactions', () => {
describe('Unconfirmed Accelerated Transaction', () => {
before(() => {
cy.intercept('/api/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
fixture: 'accelerated_tx.json'
}).as('tx');
cy.intercept('/api/v1/cpfp/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
fixture: 'accelerated_cpfp.json'
}).as('accelerated_cpfp');
cy.intercept('/api/v1/transaction-times?txId%5B%5D=40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
body: '[1723416086]',
}).as('transaction-time');
cy.intercept('https://mempool.space/api/v1/services/accelerator/accelerations/history', {
fixture: 'accelerated_history.json'
}).as('history');
cy.viewport('macbook-16');
cy.mockMempoolSocket();
cy.visit('/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a');
emitMempoolInfo({
'params': {
command: 'txPosition'
}
});
cy.waitForSkeletonGone();
});
it('shows unconfirmed accelerated transaction properly', () => {
cy.get('.badge-accelerated').should('exist');
cy.get('[data-cy="active-acceleration-box"]').should('exist');
cy.get('[data-cy="active-acceleration-box"] > table > tbody > :nth-child(1) .oobFees').invoke('text').should('contain', `15.5 `);
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `3,000`);
cy.get('#acceleration-timeline').should('be.visible');
});
// currently doesn't work due to 'accelerations/history' endpoint not being intercepted
it.skip('properly render accelerated transacion as it confirms', () => {
emitMempoolInfo({
'params': {
command: 'txPositionConfirmed'
}
});
cy.wait(1000);
cy.get('.badge-accelerated').should('exist');
cy.get('[data-cy="active-acceleration-box"]').should('not.exist');
cy.get('[data-cy="fee-rate"]').invoke('text').should('contain', `2.17 `);
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `39`);
cy.get('#acceleration-timeline').should('be.visible');
});
});
});
});

View File

@@ -1,20 +0,0 @@
{
"ancestors": [],
"bestDescendant": null,
"descendants": [],
"effectiveFeePerVsize": 15.452914798206278,
"sigops": 4,
"fee": 446,
"adjustedVsize": 223,
"acceleration": true,
"acceleratedBy": [
111,
43,
102,
112,
142,
115
],
"acceleratedAt": 1723417553,
"feeDelta": 3000
}

View File

@@ -1,24 +0,0 @@
[
{
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
"status": "completed",
"added": 1723417553,
"lastUpdated": 1723424127,
"effectiveFee": 446,
"effectiveVsize": 223,
"feeDelta": 3000,
"blockHash": "000000000000000000005bc0a822da172e43c687428cc268177ad27d636f3059",
"blockHeight": 856387,
"bidBoost": 39,
"boostVersion": "v2",
"pools": [
111,
43,
102,
112,
142,
115
],
"minedByPoolUniqueId": 111
}
]

View File

@@ -1,48 +0,0 @@
{
"txPosition": {
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
"position": {
"block": 0,
"vsize": 37321.5,
"accelerated": true
},
"accelerationPositions": [
{
"block": 0,
"vsize": 37321.5,
"poolId": 111,
"pool": "Foundry USA"
},
{
"block": 0,
"vsize": 37321.5,
"poolId": 43,
"pool": "Braiins Pool"
},
{
"block": 0,
"vsize": 37321.5,
"poolId": 102,
"pool": "SpiderPool"
},
{
"block": 0,
"vsize": 37321.5,
"poolId": 112,
"pool": "SBI Crypto"
},
{
"block": 0,
"vsize": 37321.5,
"poolId": 142,
"pool": "OCEAN"
},
{
"block": 0,
"vsize": 37321.5,
"poolId": 115,
"pool": "MARA Pool"
}
]
}
}

View File

@@ -1,66 +0,0 @@
{
"txConfirmed": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
"block":{
"id": "000000000000000000014cc3d86b7c096ef92aca180e3cf27d72e34ce944caed",
"height": 837051,
"version": 821051392,
"timestamp": 1723452588,
"bits": 386079422,
"nonce": 2215159619,
"difficulty": 90666502495565.78,
"merkle_root": "207ad51f6c1150f63fcd043eb1b4624b77ac70558594317e989c1109fbb47c47",
"tx_count": 2284,
"size": 1490522,
"weight": 3993155,
"previousblockhash": "00000000000000000002b8a66307c997aa27bf99a384ceb7cfe5f29576eddb26",
"mediantime": 1723450608,
"stale": false,
"extras": {
"reward": 319417632,
"coinbaseRaw": "0378110d04adccb9662f466f756e6472792055534120506f6f6c202364726f70676f6c642f2c08727fca05000000000000",
"orphans": [],
"medianFee": 4.021446911342697,
"feeRange": [
3.1,
3.4184397163120566,
3.998624011007912,
4.444976076555024,
5.382978723404255,
11.62814371257485,
468.75
],
"totalFees": 6917632,
"avgFee": 3030,
"avgFeeRate": 6,
"utxoSetChange": -2647,
"avgTxSize": 652.44,
"totalInputs": 8544,
"totalOutputs": 5897,
"totalOutputAmt": 2950130527407,
"segwitTotalTxs": 2084,
"segwitTotalSize": 1137877,
"segwitTotalWeight": 2582683,
"feePercentiles": null,
"virtualSize": 998288.75,
"coinbaseAddress": "bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
"coinbaseAddresses": [
"bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
"bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj"
],
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 3daaca9b82a51aca960c1491588246029d7e0fc49e0abdbcc8fd17574be5c74b",
"coinbaseSignatureAscii": "f/Foundry USA Pool #dropgold/",
"header": "0040f03026dbed7695f2e5cfb7ce84a399bf27aa97c90763a6b802000000000000000000477cb4fb09119c987e3194855570ac774b62b4b13e04cd3ff650116c1fd57a20acccb966be1a031743a70884",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 111,
"name": "Foundry USA",
"slug": "foundryusa"
},
"matchRate": 100,
"expectedFees": 6957093,
"expectedWeight": 3991895,
"similarity": 0.9907343565880212
}
}
}

View File

@@ -1,45 +0,0 @@
{
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "7c6e17739d7225d097db1f08df17d06dc712dc0951f266db1070939b85b5e8e7",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
"value": 16610556
},
"scriptsig": "483045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01210275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01 OP_PUSHBYTES_33 0275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "0014ce6c0bb00482016d12657174b6468cd01df6421e",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 ce6c0bb00482016d12657174b6468cd01df6421e",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qeekqhvqysgqk6yn9w96tv35v6qwlvss7vuvtj0",
"value": 6796193
},
{
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
"value": 9813917
}
],
"size": 223,
"weight": 892,
"sigops": 4,
"fee": 446,
"status": {
"confirmed": false
}
}

View File

@@ -96,18 +96,6 @@ export const emitMempoolInfo = ({
});
break;
}
case 'txPosition': {
cy.readFile('cypress/fixtures/accelerated_position.json', 'ascii').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
case 'txPositionConfirmed': {
cy.readFile('cypress/fixtures/accelerated_position_confirmed.json', 'ascii').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
default:
break;
}

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.24.0",
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.7.0",

View File

@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);

View File

@@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
<defs>
<style>
.d {
@@ -130,9 +130,14 @@
</svg>
<span>Unchained</span>
</a>
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
<img class="image" src="/resources/profile/bitkey.svg" />
<span>Bitkey</span>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</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">
@@ -188,19 +193,6 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://leather.io/" target="_blank" title="Leather">
<img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span>
</a>
</div>
</div>

View File

@@ -251,12 +251,3 @@
width: 64px;
height: 64px;
}
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@@ -75,7 +75,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
processing = false;
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@@ -197,11 +196,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
if (changes.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
}
}
@@ -381,10 +378,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating || this.processing) {
if (!this.canPay || this.calculating) {
return;
}
this.processing = true;
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
@@ -394,7 +390,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
@@ -402,7 +397,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('paid');
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
}
});
@@ -472,14 +466,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY
*/
async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -504,7 +494,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again
setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return;
}
this.loadingApplePay = false;
@@ -516,7 +505,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -528,7 +516,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
@@ -539,7 +526,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
@@ -551,7 +537,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
@@ -562,7 +547,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} catch (e) {
this.processing = false;
console.error(e);
}
}
@@ -573,14 +557,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY
*/
async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -615,7 +595,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -627,7 +606,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
@@ -638,7 +616,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
@@ -650,7 +627,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
@@ -668,14 +644,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* CASHAPP
*/
async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -706,7 +678,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail;
if (error) {
this.processing = false;
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$(
@@ -717,7 +688,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) {
@@ -732,7 +702,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {

View File

@@ -12,7 +12,7 @@
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>

View File

@@ -21,14 +21,14 @@
</tr>
<tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} @else {
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
}
</tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@@ -47,14 +47,13 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
<ng-container *ngFor="let pool of accelerationInfo.pools">
<img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container>
</td>
</tr>

View File

@@ -23,7 +23,6 @@
.label {
padding-right: 30px;
vertical-align: top;
}
.pool-logo {
@@ -31,8 +30,7 @@
height: 22px;
position: relative;
top: -1px;
margin-right: 4px;
margin-bottom: 4px;
margin-right: 3px;
}
.oobFees {

View File

@@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar',
barWidth: '90%',
large: true,
barMinHeight: 3,
barMinHeight: 1,
},
],
dataZoom: (this.widget || data.length === 0 )? undefined : [{

View File

@@ -33,7 +33,7 @@
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td>
<td class="bid text-right">
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -41,7 +41,7 @@
</ng-container>
<ng-container *ngIf="!pending">
<td *ngIf="acceleration.boost != null" class="fee text-right">
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td *ngIf="acceleration.boost == null" class="fee text-right">
~

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@@ -61,11 +61,8 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = combineLatest([
this.route.params,
timer(0),
]).pipe(
tap(([params]) => {
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})

View File

@@ -10,10 +10,10 @@
</td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
@@ -23,8 +23,7 @@ function toRGB({r,g,b}): string {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActiveAccelerationBox implements OnChanges {
@Input() acceleratedBy?: number[];
@Input() effectiveFeeRate?: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats;
@Input() pools: number[];
@@ -42,12 +41,10 @@ export class ActiveAccelerationBox implements OnChanges {
timespan = '';
chartInstance: any = undefined;
constructor(
private cd: ChangeDetectorRef,
) {}
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
if (pools && this.miningStats) {
this.prepareChartOptions(pools);
}
@@ -135,7 +132,6 @@ export class ActiveAccelerationBox implements OnChanges {
}
]
};
this.cd.markForCheck();
}
onChartInit(ec) {

View File

@@ -94,20 +94,6 @@
</div>
</ng-container>
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
</div>
<div class="box">
<div class="row">
<div class="col-md">
<app-utxo-graph [utxos]="utxos" left="80" />
</div>
</div>
</div>
</ng-container>
<br>
<div class="title-tx">
<h2 class="text-left">

View File

@@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@@ -104,7 +104,6 @@ export class AddressComponent implements OnInit, OnDestroy {
addressString: string;
isLoadingAddress = true;
transactions: Transaction[];
utxos: Utxo[];
isLoadingTransactions = true;
retryLoadMore = false;
error: any;
@@ -160,7 +159,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.address = null;
this.isLoadingTransactions = true;
this.transactions = null;
this.utxos = null;
this.addressInfo = null;
this.exampleChannel = null;
document.body.scrollTo(0, 0);
@@ -214,19 +212,11 @@ export class AddressComponent implements OnInit, OnDestroy {
this.updateChainStats();
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
return forkJoin([
address.is_pubkey
return address.is_pubkey
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address),
utxoCount >= 2 && utxoCount <= 500 ? (address.is_pubkey
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressUtxos$(address.address)) : of([])
]);
: this.electrsApiService.getAddressTransactions$(address.address);
}),
switchMap(([transactions, utxos]) => {
this.utxos = utxos;
switchMap((transactions) => {
this.tempTransactions = transactions;
if (transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
@@ -344,23 +334,6 @@ export class AddressComponent implements OnInit, OnDestroy {
}
}
// update utxos in-place
for (const vin of transaction.vin) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: transaction.txid,
vout: index,
value: vout.value,
status: JSON.parse(JSON.stringify(transaction.status)),
});
}
}
return true;
}
@@ -373,26 +346,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
// update utxos in-place
for (const vin of transaction.vin) {
if (vin.prevout?.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: vin.txid,
vout: vin.vout,
value: vin.prevout.value,
status: { confirmed: true }, // Assuming the input was confirmed
});
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
}
}
}
return true;
}

View File

@@ -1,7 +0,0 @@
<div [formGroup]="amountForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 70px;" (change)="changeMode()">
<option value="btc" i18n="shared.btc|BTC">BTC</option>
<option value="sats" i18n="shared.sats">sats</option>
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
</select>
</div>

View File

@@ -1,36 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-amount-selector',
templateUrl: './amount-selector.component.html',
styleUrls: ['./amount-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AmountSelectorComponent implements OnInit {
amountForm: UntypedFormGroup;
modes = ['btc', 'sats', 'fiat'];
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.amountForm = this.formBuilder.group({
mode: ['btc']
});
this.stateService.viewAmountMode$.subscribe((mode) => {
this.amountForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.amountForm.get('mode')?.value;
this.storageService.setValue('view-amount-mode', newMode);
this.stateService.viewAmountMode$.next(newMode);
}
}

View File

@@ -40,7 +40,7 @@
</tr>
<tr>
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sats">sats</span> &nbsp; <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> &nbsp; <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
</tr>
<tr>
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>

View File

@@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))

View File

@@ -319,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.accelerationsSubscription = this.block$.pipe(
switchMap((block) => {
return this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
@@ -327,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy {
})
).subscribe((accelerations) => {
this.accelerations = accelerations;
if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription
if (accelerations.length) {
this.setupBlockAudit();
}
});

View File

@@ -1,10 +1,7 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1 i18n="master-page.blocks">Blocks</h1>
<app-svg-images name="blocks-2-3" style="width: 275px; max-width: 90%; margin-top: -10px"></app-svg-images>
</div>
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>

View File

@@ -12,7 +12,7 @@
<div class="input-group-prepend">
<span class="input-group-text">{{ currency$ | async }}</span>
</div>
<input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
@@ -20,7 +20,7 @@
<div class="input-group-prepend">
<span class="input-group-text">BTC</span>
</div>
<input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
@@ -28,7 +28,7 @@
<div class="input-group-prepend">
<span class="input-group-text" i18n="shared.sats">sats</span>
</div>
<input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
</form>

View File

@@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks,
remainingBlocks: da.remainingBlocks - 1,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,

View File

@@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
minedBlocks: this.currentIndex,
remainingBlocks: da.remainingBlocks,
minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments,
colorPreviousAdjustments,

View File

@@ -5,7 +5,7 @@
</div>
<div class="faucet-container text-center">
@if (txid) {
<div class="alert alert-success w-100 text-truncate">
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
@@ -36,13 +36,6 @@
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div>
}
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
</div>
</div>
}
@else if (error) {
<!-- User can request -->
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
@@ -88,7 +81,7 @@
}
<!-- Send back coins -->
@if (status?.address) {
@if (status?.address) {
<div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
}

View File

@@ -19,7 +19,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
error: string = '';
user: any = undefined;
txid: string = '';
faucetStatusSubscription: Subscription;
status: {
min: number; // minimum amount to request at once (in sats)

View File

@@ -19,7 +19,7 @@
</tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ rbfInfo.tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
<td>{{ rbfInfo.tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr>
<tr *only-vsize>
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,5 @@
<div class="container-xl">
<div style="display: flex; width: 100%; align-items: center; flex-wrap: wrap;">
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
<app-svg-images name="blocks-3-2" style="width: 275px; max-width: 90%; margin-top: -9px"></app-svg-images>
</div>
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>

View File

@@ -42,7 +42,7 @@
<div class="blockchain-wrapper" [style]="{ height: blockchainHeight * 1.16 + 'px' }">
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
</div>
<div class="panel" *ngIf="!error || waitingForTransaction">
<div class="panel">
@if (replaced) {
<div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
@@ -65,25 +65,23 @@
}
</div>
</div>
@if (!replaced) {
<div class="field narrower">
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
<div class="value">
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
<span class="justify-content-end d-flex align-items-center">
@if (eta.blocks >= 7) {
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
</span>
</ng-container>
<ng-template #etaSkeleton>
<span class="skeleton-loader" style="max-width: 200px;"></span>
</ng-template>
</div>
</div>
}
<div class="field narrower">
<div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
<div class="value">
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
<span class="justify-content-end d-flex align-items-center">
@if (eta.blocks >= 7) {
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
</span>
</ng-container>
<ng-template #etaSkeleton>
<span class="skeleton-loader" style="max-width: 200px;"></span>
</ng-template>
</div>
</div>
} @else if (tx && tx.status?.confirmed) {
<div class="field narrower mt-2">
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
@@ -113,7 +111,7 @@
</div>
</div>
<div class="bottom-panel" *ngIf="!error || waitingForTransaction">
<div class="bottom-panel">
@if (isLoading) {
<div class="progress-icon">
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
@@ -186,12 +184,6 @@
</div>
}
</div>
<div class="bottom-panel" *ngIf="error && !waitingForTransaction">
<app-http-error [error]="error">
<span i18n="transaction.error.loading-transaction-data">Error loading transaction data.</span>
</app-http-error>
</div>
<div class="footer-link"
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"

View File

@@ -286,7 +286,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.accelerationInfo = null;
}),
switchMap((blockHash: string) => {
return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId);
return this.servicesApiService.getAccelerationHistory$({ blockHash });
}),
catchError(() => {
return of(null);

View File

@@ -95,7 +95,7 @@
<p>The mempool Square Logo</p>
<br><br>
<app-svg-images name="accelerator" style="width: 500px; max-width: 80%"></app-svg-images>
<app-svg-images name="accelerator" height="76px"></app-svg-images>
<br><br>
<p>The Mempool Accelerator Logo</p>
<br><br>

View File

@@ -21,7 +21,7 @@
</ng-template>
</span>
<span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">&lrm;{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
</div>

View File

@@ -551,24 +551,24 @@
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@if (network === 'liquid' || network === 'liquidtestnet') {
@if (eta.blocks >= 7) {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @else {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) ? 'etaDeepMempool d-flex justify-content-between' : ''">
@if (eta.blocks >= 7) {
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
<div class="d-flex accelerate">
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&trade; tooltip" ngbTooltip="This transaction cannot be accelerated">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</div>
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
<span class="eta justify-content-end">
</span>
}
</ng-container>
<ng-template #etaSkeleton>
@@ -606,9 +606,9 @@
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" [attr.data-cy]="'tx-fee-delta'" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td>
@@ -647,9 +647,9 @@
<td>
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
<app-fee-rate [attr.data-cy]="'fee-rate'" [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [attr.data-cy]="'fee-rate'" [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
@@ -670,7 +670,7 @@
<ng-template #acceleratingRow>
<tr>
<td rowspan="2" colspan="2" style="padding: 0;">
<app-active-acceleration-box [attr.data-cy]="'active-acceleration-box'" [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
</td>
</tr>
<tr></tr>

View File

@@ -287,21 +287,37 @@
}
.accelerate {
@media (min-width: 850px) {
margin-left: auto;
}
display: flex !important;
align-self: auto;
margin-left: auto;
background-color: var(--tertiary);
@media (max-width: 849px) {
margin-left: 5px;
}
}
.etaDeepMempool {
justify-content: flex-end;
flex-wrap: wrap;
align-content: center;
@media (max-width: 995px) {
justify-content: left !important;
}
@media (max-width: 849px) {
justify-content: right !important;
}
}
.accelerateDeepMempool {
align-self: auto;
margin-left: auto;
background-color: var(--tertiary);
margin-left: 5px;
@media (max-width: 995px) {
margin-left: 0px;
}
@media (max-width: 849px) {
margin-left: 5px;
}
}
.goggles-icon {
@@ -319,9 +335,4 @@
.oobFees {
color: #905cf4;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}

View File

@@ -139,7 +139,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
firstLoad = true;
waitingForAccelerationInfo: boolean = false;
isLoadingFirstSeen = false;
notAcceleratedOnLoad: boolean = null;
featuresEnabled: boolean;
segwitEnabled: boolean;
@@ -192,7 +191,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
if (!this.stateService.isLiquid()) {
this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
}
@@ -344,7 +343,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setIsAccelerated();
}),
switchMap((blockHeight: number) => {
return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe(
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
switchMap((accelerationHistory: Acceleration[]) => {
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
return throwError('retry');
@@ -491,7 +490,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.stateService.network === '') {
if (!this.mempoolPosition.accelerated) {
if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningService.getMiningStats('1w').subscribe(stats => {
this.miningStats = stats;
});
}
@@ -849,10 +848,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp);
}
if (this.notAcceleratedOnLoad === null) {
this.notAcceleratedOnLoad = !this.isAcceleration;
}
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
this.forceAccelerationSummary = true;
@@ -971,7 +966,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.filters = [];
this.showCpfpDetails = false;
this.showAccelerationDetails = false;
this.accelerationFlowCompleted = false;
this.accelerationInfo = null;
this.cashappEligible = false;
this.txInBlockIndex = null;
@@ -1089,7 +1083,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
(!this.hideAccelerationSummary && !this.accelerationFlowCompleted)
|| this.forceAccelerationSummary
)
&& this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs
);
}

View File

@@ -321,7 +321,7 @@
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
<app-fee-rate [fee]="tx.fee" [weight]="tx.weight"></app-fee-rate>
<span class="d-none d-sm-inline-block">&nbsp;&ndash; {{ tx.fee | number }} <span class="symbol"
i18n="shared.sats">sats</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></span>
i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></span>
</div>
<div class="float-left mt-2-5 grey-info-text" *ngIf="tx.fee === -1" i18n="transactions-list.load-to-reveal-fee-info">Show more inputs to reveal fee data</div>

View File

@@ -1,21 +0,0 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget">
<ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, paddingBottom: !widget}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</ng-container>
<ng-container *ngIf="error">
<div class="error-wrapper">
<p class="error">{{ error }}</p>
</div>
</ng-container>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -1,59 +0,0 @@
.card-header {
border-bottom: 0;
font-size: 18px;
@media (min-width: 465px) {
font-size: 20px;
}
@media (min-width: 992px) {
height: 40px;
}
}
.main-title {
position: relative;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
display: flex;
flex-direction: column;
padding: 0px;
width: 100%;
height: 400px;
}
.error-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
font-size: 15px;
color: grey;
font-weight: bold;
}
.chart {
display: flex;
flex: 1;
width: 100%;
padding-right: 10px;
}
.chart-widget {
width: 100%;
height: 100%;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}

View File

@@ -1,285 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts';
import { BehaviorSubject, Subscription } from 'rxjs';
import { Utxo } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { renderSats } from '../../shared/common.utils';
@Component({
selector: 'app-utxo-graph',
templateUrl: './utxo-graph.component.html',
styleUrls: ['./utxo-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UtxoGraphComponent implements OnChanges, OnDestroy {
@Input() utxos: Utxo[];
@Input() height: number = 200;
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
error: any;
isLoading = true;
chartInstance: any = undefined;
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
private zone: NgZone,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
) {}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.utxos) {
return;
}
if (changes.utxos) {
this.prepareChartOptions(this.utxos);
}
}
prepareChartOptions(utxos: Utxo[]) {
if (!utxos || utxos.length === 0) {
return;
}
this.isLoading = false;
// Helper functions
const distance = (x1: number, y1: number, x2: number, y2: number): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const intersectionPoints = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): [number, number][] => {
const d = distance(x1, y1, x2, y2);
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
const h = Math.sqrt(r1 * r1 - a * a);
const x3 = x1 + a * (x2 - x1) / d;
const y3 = y1 + a * (y2 - y1) / d;
return [
[x3 + h * (y2 - y1) / d, y3 - h * (x2 - x1) / d],
[x3 - h * (y2 - y1) / d, y3 + h * (x2 - x1) / d]
];
};
// Naive algorithm to pack circles as tightly as possible without overlaps
const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = [];
// Pack in descending order of value, and limit to the top 500 to preserve performance
const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500);
let centerOfMass = { x: 0, y: 0 };
let weightOfMass = 0;
sortedUtxos.forEach((utxo, index) => {
// area proportional to value
const r = Math.sqrt(utxo.value);
// special cases for the first two utxos
if (index === 0) {
placedCircles.push({ x: 0, y: 0, r, utxo, distances: [0] });
return;
}
if (index === 1) {
const c = placedCircles[0];
placedCircles.push({ x: c.r + r, y: 0, r, utxo, distances: [c.r + r, 0] });
c.distances.push(c.r + r);
return;
}
// The best position will be touching two other circles
// generate a list of candidate points by finding all such positions
// where the circle can be placed without overlapping other circles
const candidates: [number, number, number[]][] = [];
const numCircles = placedCircles.length;
for (let i = 0; i < numCircles; i++) {
for (let j = i + 1; j < numCircles; j++) {
const c1 = placedCircles[i];
const c2 = placedCircles[j];
if (c1.distances[j] > (c1.r + c2.r + r + r)) {
// too far apart for new circle to touch both
continue;
}
const points = intersectionPoints(c1.x, c1.y, c1.r + r, c2.x, c2.y, c2.r + r);
points.forEach(([x, y]) => {
const distances: number[] = [];
let valid = true;
for (let k = 0; k < numCircles; k++) {
const c = placedCircles[k];
const d = distance(x, y, c.x, c.y);
if (k !== i && k !== j && d < (r + c.r)) {
valid = false;
break;
} else {
distances.push(d);
}
}
if (valid) {
candidates.push([x, y, distances]);
}
});
}
}
// Pick the candidate closest to the center of mass
const [x, y, distances] = candidates.length ? candidates.reduce((closest, candidate) =>
distance(candidate[0], candidate[1], centerOfMass[0], centerOfMass[1]) <
distance(closest[0], closest[1], centerOfMass[0], centerOfMass[1])
? candidate
: closest
) : [0, 0, []];
placedCircles.push({ x, y, r, utxo, distances });
for (let i = 0; i < distances.length; i++) {
placedCircles[i].distances.push(distances[i]);
}
distances.push(0);
// Update center of mass
centerOfMass = {
x: (centerOfMass.x * weightOfMass + x) / (weightOfMass + r),
y: (centerOfMass.y * weightOfMass + y) / (weightOfMass + r),
};
weightOfMass += r;
});
// Precompute the bounding box of the graph
const minX = Math.min(...placedCircles.map(d => d.x - d.r));
const maxX = Math.max(...placedCircles.map(d => d.x + d.r));
const minY = Math.min(...placedCircles.map(d => d.y - d.r));
const maxY = Math.max(...placedCircles.map(d => d.y + d.r));
const width = maxX - minX;
const height = maxY - minY;
const data = placedCircles.map((circle, index) => [
circle.utxo,
index,
circle.x,
circle.y,
circle.r
]);
this.chartOptions = {
series: [{
type: 'custom',
coordinateSystem: undefined,
data,
renderItem: (params, api) => {
const idx = params.dataIndex;
const datum = data[idx];
const utxo = datum[0] as Utxo;
const chartWidth = api.getWidth();
const chartHeight = api.getHeight();
const scale = Math.min(chartWidth / width, chartHeight / height);
const scaledWidth = width * scale;
const scaledHeight = height * scale;
const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale;
const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale;
const x = datum[2] as number;
const y = datum[3] as number;
const r = datum[4] as number;
if (r * scale < 3) {
// skip items too small to render cleanly
return;
}
const valueStr = renderSats(utxo.value, this.stateService.network);
const elements: any[] = [
{
type: 'circle',
autoBatch: true,
shape: {
cx: (x * scale) + offsetX,
cy: (y * scale) + offsetY,
r: (r * scale) - 1,
},
style: {
fill: '#5470c6',
}
},
];
const labelFontSize = Math.min(36, r * scale * 0.25);
if (labelFontSize > 8) {
elements.push({
type: 'text',
x: (x * scale) + offsetX,
y: (y * scale) + offsetY,
style: {
text: valueStr,
fontSize: labelFontSize,
fill: '#fff',
align: 'center',
verticalAlign: 'middle',
},
});
}
return {
type: 'group',
children: elements,
};
}
}],
tooltip: {
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: 'var(--tooltip-grey)',
align: 'left',
},
borderColor: '#000',
formatter: (params: any): string => {
const utxo = params.data[0] as Utxo;
const valueStr = renderSats(utxo.value, this.stateService.network);
return `
<b style="color: white;">${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}</b>
<br>
${valueStr}`;
},
}
};
this.cd.markForCheck();
}
onChartClick(e): void {
if (e.data?.[0]?.txid) {
this.zone.run(() => {
const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url + '?mode=details#vout=' + e.data[0].vout);
} else {
this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` });
}
});
}
}
onChartInit(ec): void {
this.chartInstance = ec;
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isMobile(): boolean {
return (window.innerWidth <= 767.98);
}
}

View File

@@ -1,6 +1,6 @@
// Import tree-shakeable echarts
import * as echarts from 'echarts/core';
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
// Typescript interfaces
@@ -12,7 +12,6 @@ echarts.use([
TitleComponent, TooltipComponent, GridComponent,
LegendComponent, GeoComponent, DataZoomComponent,
VisualMapComponent, MarkLineComponent,
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
CustomChart,
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
]);
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };

View File

@@ -36,7 +36,6 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
import { AddressComponent } from '../components/address/address.component';
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component';
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
import { CommonModule } from '@angular/common';
@@ -77,7 +76,6 @@ import { CommonModule } from '@angular/common';
HashrateChartPoolsComponent,
BlockHealthGraphComponent,
AddressGraphComponent,
UtxoGraphComponent,
ActiveAccelerationBox,
],
imports: [

View File

@@ -233,10 +233,3 @@ interface AssetStats {
peg_out_amount: number;
burn_count: number;
}
export interface Utxo {
txid: string;
vout: number;
value: number;
status: Status;
}

View File

@@ -13,8 +13,7 @@ class GuardService {
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98;
}
}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface';
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface';
import { StateService } from './state.service';
import { BlockExtended } from '../interfaces/node-api.interface';
import { calcScriptHash$ } from '../bitcoin.utils';
@@ -166,16 +166,6 @@ export class ElectrsApiService {
);
}
getAddressUtxos$(address: string): Observable<Utxo[]> {
return this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo');
}
getScriptHashUtxos$(script: string): Observable<Utxo[]> {
return from(calcScriptHash$(script)).pipe(
switchMap(scriptHash => this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')),
);
}
getAsset$(assetId: string): Observable<Asset> {
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
}

View File

@@ -28,7 +28,7 @@ export class EtaService {
return combineLatest([
this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
this.stateService.difficultyAdjustment$,
miningStats ? of(miningStats) : this.miningService.getMiningStats('1m'),
miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'),
]).pipe(
map(([mempoolPosition, da, miningStats]) => {
if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
@@ -166,7 +166,7 @@ export class EtaService {
pools[pool.poolUniqueId] = pool;
}
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId]?.lastEstimatedHashrate || 0), 0);
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
const shares = [
{
block: unacceleratedPosition.block,
@@ -174,7 +174,7 @@ export class EtaService {
},
...accelerationPositions.map(pos => ({
block: pos.block,
hashrateShare: ((pools[pos.poolId]?.lastEstimatedHashrate || 0) / miningStats.lastEstimatedHashrate)
hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate)
}))
];
return this.calculateETAFromShares(shares, da);
@@ -204,7 +204,7 @@ export class EtaService {
let tailProb = 0;
let Q = 0;
for (let i = 0; i <= max; i++) {
for (let i = 0; i < max; i++) {
// find H_i
const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0);
// find S_i
@@ -215,7 +215,7 @@ export class EtaService {
tailProb += S;
}
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
Q += ((max + 1) * (1-tailProb));
Q += (1-tailProb);
const eta = da.timeAvg * Q; // T x Q
return {

View File

@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { StateService } from './state.service';
import { StorageService } from './storage.service';
import { MenuGroup } from '../interfaces/services.interface';
import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap, map } from 'rxjs';
import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs';
import { IBackendInfo } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
@@ -160,29 +160,6 @@ export class ServicesApiServices {
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } });
}
getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable<Acceleration[]> {
const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => {
return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe(
map((response) => ({
page,
total: parseInt(response.headers.get('X-Total-Count'), 10),
accelerations: accelerations.concat(response.body || []),
})),
switchMap(({page, total, accelerations}) => {
if (accelerations.length >= Math.min(total, limit ?? Infinity) || (findTxid && accelerations.find((acc) => acc.txid === findTxid))) {
return of({ page, total, accelerations });
} else {
return getPage$(page + 1, accelerations);
}
}),
);
};
return getPage$(1).pipe(
map(({ accelerations }) => accelerations),
);
}
getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> {
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
}

View File

@@ -1,7 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
import { TransactionStripped } from "../interfaces/node-api.interface";
import { AmountShortenerPipe } from "./pipes/amount-shortener.pipe";
const amountShortenerPipe = new AmountShortenerPipe();
export function isMobile(): boolean {
return (window.innerWidth <= 767.98);
@@ -186,33 +184,6 @@ export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCom
};
}
export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string {
let prefix = '';
switch (network) {
case 'liquid':
prefix = 'L';
break;
case 'liquidtestnet':
prefix = 'tL';
break;
case 'testnet':
case 'testnet4':
prefix = 't';
break;
case 'signet':
prefix = 's';
break;
}
if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) {
return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`;
} else {
if (prefix.length) {
prefix += '-';
}
return `${amountShortenerPipe.transform(value)} ${prefix}sats`;
}
}
export function insecureRandomUUID(): string {
const hexDigits = '0123456789abcdef';
const uuidLengths = [8, 4, 4, 4, 12];

View File

@@ -13,13 +13,8 @@
</div>
@if (!enterpriseInfo?.footer_img) {
<p class="explore-tagline-mobile">
@if (officialMempoolSpace) {
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
} @else {
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&trade;</ng-template>
}
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
</p>
}
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
@@ -32,38 +27,29 @@
<div class="selector">
<app-rate-unit-selector></app-rate-unit-selector>
</div>
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-amount-selector></app-amount-selector>
</div>
@if (!env.customize?.theme) {
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<div class="selector d-none d-sm-flex">
<app-theme-selector></app-theme-selector>
</div>
}
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none justify-content-center" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'" [routerLink]="['/login']">
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a>
</div>
@if (!env.customize?.theme) {
<div class="selector d-flex justify-content-center ml-auto mr-auto mt-0" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'">
<app-amount-selector class="add-margin"></app-amount-selector>
<app-theme-selector class="add-margin"></app-theme-selector>
<div class="selector d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0">
<app-theme-selector></app-theme-selector>
</div>
}
@if (!enterpriseInfo?.footer_img) {
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex justify-content-center ml-auto mr-auto mt-0 mb-2" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'" [routerLink]="['/login']">
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a>
<p class="explore-tagline-desktop">
@if (officialMempoolSpace) {
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
} @else {
<ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&trade;</ng-template>
}
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">&reg;</ng-template>
</p>
}
</div>

View File

@@ -76,11 +76,6 @@ footer .selector {
display: inline-block;
}
footer .add-margin {
margin-left: 5px;
margin-right: 5px;
}
footer .row.link-tree {
max-width: 1140px;
margin: 0 auto;
@@ -159,7 +154,7 @@ footer .nowrap {
display: block;
}
@media (min-width: 1020px) {
@media (min-width: 951px) {
:host-context(.ltr-layout) .language-selector {
float: right !important;
}
@@ -177,24 +172,7 @@ footer .nowrap {
}
.services {
@media (min-width: 1300px) {
:host-context(.ltr-layout) .language-selector {
float: right !important;
}
:host-context(.rtl-layout) .language-selector {
float: left !important;
}
.explore-tagline-desktop {
display: block;
}
.explore-tagline-mobile {
display: none;
}
}
@media (max-width: 1300px) {
@media (min-width: 951px) and (max-width: 1147px) {
:host-context(.ltr-layout) .services .language-selector {
float: none !important;
}
@@ -270,7 +248,7 @@ footer .nowrap {
}
@media (max-width: 1019px) {
@media (max-width: 950px) {
.main-logo {
width: 220px;
@@ -309,7 +287,7 @@ footer .nowrap {
}
}
@media (max-width: 1300px) {
@media (max-width: 1147px) {
.services.main-logo {
width: 220px;

View File

@@ -267,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opN) {
return;
}
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -287,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opM) {
return;
}
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
if (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark} from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@@ -35,7 +35,6 @@ import { LanguageSelectorComponent } from '../components/language-selector/langu
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component';
import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component';
import { AmountSelectorComponent } from '../components/amount-selector/amount-selector.component';
import { BrowserOnlyDirective } from './directives/browser-only.directive';
import { ServerOnlyDirective } from './directives/server-only.directive';
import { ColoredPriceDirective } from './directives/colored-price.directive';
@@ -132,7 +131,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent,
ThemeSelectorComponent,
RateUnitSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe,
RelativeUrlPipe,
NoSanitizePipe,
@@ -280,7 +278,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent,
RateUnitSelectorComponent,
ThemeSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe,
RelativeUrlPipe,
Hex2asciiPipe,
@@ -443,6 +440,5 @@ export class SharedModule {
library.addIcons(faFaucetDrip);
library.addIcons(faTimeline);
library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck);
}
}

View File

@@ -1,3 +0,0 @@
<svg width="212" height="450" viewBox="0 0 212 450" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.005 2.14659C109.052 -0.71553 102.948 -0.715531 97.9948 2.14659L7.99481 54.1531C3.04732 57.012 0 62.2924 0 68.0065V172.043C0 177.758 3.04733 183.038 7.99482 185.897L69.0247 221.163C73.9722 224.022 77.0195 229.302 77.0195 235.016V433.998V437.998C77.0195 444.625 82.3921 449.998 89.0195 449.998H93.0195H118.981H123.031C129.658 449.998 135.031 444.625 135.031 437.998V233.751C135.444 228.531 138.395 223.809 142.975 221.163L204.005 185.897C208.953 183.038 212 177.758 212 172.043V68.0065C212 62.2924 208.953 57.012 204.005 54.1531L114.005 2.14659ZM112.07 68.1162C108.334 65.938 103.716 65.938 99.9803 68.1162L63.9718 89.1129C60.2841 91.2631 58.0164 95.2105 58.0164 99.4793V141.68C58.0164 145.948 60.2841 149.896 63.9718 152.046L99.9803 173.043C103.716 175.221 108.334 175.221 112.07 173.043L148.078 152.046C151.766 149.896 154.034 145.948 154.034 141.68V99.4793C154.034 95.2105 151.766 91.2631 148.078 89.1129L112.07 68.1162ZM172.046 338.527C164.047 333.861 154 339.631 154 348.892L154 356L154 412V419.108C154 428.369 164.047 434.14 172.046 429.473L196.046 415.473C199.733 413.322 202 409.376 202 405.108V362.892C202 358.624 199.733 354.678 196.046 352.527L172.046 338.527Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="387" height="377" viewBox="-58.05 -56.55 503.1 490.1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M249.327 137.966C283.745 132.618 333.52 96.2553 333.52 67.9135C333.52 59.3574 326.637 53.4752 316.576 53.4752C297.513 53.4752 265.212 82.3518 249.327 137.966ZM89.9409 278.606C44.9317 278.606 41.225 323.525 86.2343 323.525C106.356 323.525 130.714 315.504 143.422 301.065C124.889 285.023 109.533 278.606 89.9409 278.606ZM376.411 259.355C379.059 334.755 340.934 377 276.332 377C238.207 377 219.144 362.562 178.371 335.824C157.19 359.353 116.946 377 83.5867 377C-31.3192 377 -26.5536 229.943 90.4704 229.943C114.828 229.943 135.48 236.36 161.956 252.938L179.43 191.441C107.415 171.655 71.4077 116.041 106.886 36.3631H164.074C132.303 89.3035 154.013 133.153 194.256 137.966C215.967 60.4269 262.565 0 324.518 0C359.467 0 387.002 22.9943 387.002 64.705C387.002 131.549 300.161 186.094 234.5 191.441L207.494 287.162C238.207 322.99 323.459 357.749 323.459 259.355H376.411Z" fill="#F5F1ED"/>
</svg>

Before

Width:  |  Height:  |  Size: 1011 B

View File

@@ -15,7 +15,6 @@ rpcpassword=__BITCOIN_RPC_PASS__
whitelist=127.0.0.1
whitelist=103.99.168.0/22
whitelist=2401:b140::/32
blocksxor=0
#uacomment=@wiz
[main]

View File

@@ -392,9 +392,9 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba
# packages needed for mempool ecosystem
FREEBSD_PKG=()
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
FREEBSD_PKG+=(openssh-portable py311-pip rust llvm18 jq base64 libzmq4)
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4)
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server)
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase)
FREEBSD_PKG+=(geoipupdate redis)
FREEBSD_UNFURL_PKG=()

View File

@@ -1,5 +1,3 @@
#!/usr/bin/env zsh
rm -f $HOME/*/backend/mempool-config.json
rm -f $HOME/*/frontend/mempool-frontend-config.json
rm -f $HOME/*/frontend/projects/mempool/mempool-frontend-config.json
exit 0
rm $HOME/*/backend/mempool-config.json
rm $HOME/*/frontend/mempool-frontend-config.json