Merge branch 'master' into natsoni/federation-utxos-expiry

This commit is contained in:
natsoni 2024-03-07 10:27:44 +01:00 committed by GitHub
commit e8c5d478fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 1202 additions and 137 deletions

View file

@ -0,0 +1,19 @@
name: 'Check if servers are in sync'
on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
name: Get block height
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: repo
- name: Run script
working-directory: repo
run: |
chmod +x ./scripts/get_block_tip_height.sh
sh ./scripts/get_block_tip_height.sh

85
Cargo.lock generated
View file

@ -57,9 +57,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901"
dependencies = [
"quote",
"syn 2.0.20",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "gbt"
version = "1.0.0"
@ -71,15 +77,15 @@ dependencies = [
"napi-derive",
"priority-queue",
"tracing",
"tracing-log",
"tracing-log 0.2.0",
"tracing-subscriber",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hermit-abi"
@ -92,11 +98,11 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.9.3"
version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"autocfg",
"equivalent",
"hashbrown",
]
@ -114,12 +120,12 @@ checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
[[package]]
name = "libloading"
version = "0.7.4"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
dependencies = [
"cfg-if",
"winapi",
"windows-targets",
]
[[package]]
@ -145,9 +151,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "napi"
version = "2.13.2"
version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84"
dependencies = [
"bitflags",
"ctor",
@ -159,29 +165,29 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.0.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e"
checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
[[package]]
name = "napi-derive"
version = "2.13.0"
version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367"
checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce"
dependencies = [
"cfg-if",
"convert_case",
"napi-derive-backend",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
name = "napi-derive-backend"
version = "1.0.52"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17"
checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4"
dependencies = [
"convert_case",
"once_cell",
@ -189,14 +195,14 @@ dependencies = [
"quote",
"regex",
"semver",
"syn 1.0.109",
"syn",
]
[[package]]
name = "napi-sys"
version = "2.2.3"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3"
checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
dependencies = [
"libloading",
]
@ -223,9 +229,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "overload"
@ -241,11 +247,12 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "priority-queue"
version = "1.3.2"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61"
checksum = "509354d8a769e8d0b567d6821b84495c60213162761a732d68ce87c964bd347f"
dependencies = [
"autocfg",
"equivalent",
"indexmap",
]
@ -320,17 +327,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.20"
@ -384,7 +380,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.20",
"syn",
]
[[package]]
@ -408,6 +404,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
@ -423,7 +430,7 @@ dependencies = [
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-log 0.1.3",
]
[[package]]

View file

@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
"./backend/rust-gbt",
]

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": false,
"NETWORK": "mainnet",
"BACKEND": "electrum",
"ENABLED": true,

View file

@ -9,6 +9,7 @@
"version": "3.0.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.24.0",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.6.1",
@ -1499,9 +1500,9 @@
}
},
"node_modules/@napi-rs/cli": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
"bin": {
"napi": "scripts/index.js"
},
@ -7669,7 +7670,7 @@
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
"@napi-rs/cli": "2.16.1"
"@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"
@ -8774,9 +8775,9 @@
"integrity": "sha512-mlvPiCzUlaETpYW3i6V87A24jjMYgsebaXtUo3WQyyLnYUuxs0KiXQ2mnKh3h15j8Xg/hfxeGIi+5OC9u0nftQ=="
},
"@napi-rs/cli": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA=="
},
"@noble/hashes": {
"version": "1.3.0",
@ -12702,7 +12703,7 @@
"rust-gbt": {
"version": "file:rust-gbt",
"requires": {
"@napi-rs/cli": "2.16.1"
"@napi-rs/cli": "2.18.0"
}
},
"safe-buffer": {

View file

@ -12,14 +12,14 @@ crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
priority-queue = "1.3.2"
priority-queue = "2.0.2"
bytes = "1.4.0"
napi = { version = "2.13.2", features = ["napi8", "tokio_rt"] }
napi-derive = "2.13.0"
napi = { version = "2.16.0", features = ["napi8", "tokio_rt"] }
napi-derive = "2.16.0"
bytemuck = "1.13.1"
tracing = "0.1.36"
tracing-log = "0.1.3"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"]}
[build-dependencies]
napi-build = "2.0.1"
napi-build = "2.1.2"

View file

@ -237,6 +237,49 @@ switch (platform) {
loadError = e
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'gbt.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./gbt.linux-riscv64-musl.node')
} else {
nativeBinding = require('gbt-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'gbt.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./gbt.linux-riscv64-gnu.node')
} else {
nativeBinding = require('gbt-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'gbt.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./gbt.linux-s390x-gnu.node')
} else {
nativeBinding = require('gbt-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}

View file

@ -9,16 +9,16 @@
"version": "3.0.1",
"hasInstallScript": true,
"dependencies": {
"@napi-rs/cli": "2.16.1"
"@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/@napi-rs/cli": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz",
"integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==",
"bin": {
"napi": "scripts/index.js"
},

View file

@ -25,7 +25,7 @@
}
},
"dependencies": {
"@napi-rs/cli": "2.16.1"
"@napi-rs/cli": "2.18.0"
},
"engines": {
"node": ">= 12"

View file

@ -1,6 +1,7 @@
{
"MEMPOOL": {
"ENABLED": true,
"OFFICIAL": false,
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"BLOCKS_SUMMARIES_INDEXING": true,
@ -79,7 +80,8 @@
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__",
"PID_DIR": "__DATABASE_PID_FILE__",
"TIMEOUT": 3000
"TIMEOUT": 3000,
"POOL_SIZE": 100
},
"SYSLOG": {
"ENABLED": false,

View file

@ -14,6 +14,7 @@ describe('Mempool Backend Config', () => {
expect(config.MEMPOOL).toStrictEqual({
ENABLED: true,
OFFICIAL: false,
NETWORK: 'mainnet',
BACKEND: 'none',
BLOCKS_SUMMARIES_INDEXING: false,
@ -93,7 +94,8 @@ describe('Mempool Backend Config', () => {
USERNAME: 'mempool',
PASSWORD: 'mempool',
TIMEOUT: 180000,
PID_DIR: ''
PID_DIR: '',
POOL_SIZE: 100,
});
expect(config.SYSLOG).toStrictEqual({

View file

@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[];
}
export interface BitcoinRpcCredentials {
host: string;
@ -38,3 +39,15 @@ export interface BitcoinRpcCredentials {
timeout: number;
cookie?: string;
}
export interface HealthCheckHost {
host: string;
active: boolean;
rtt: number;
latestHeight: number;
socket: boolean;
outOfSync: boolean;
unreachable: boolean;
checked: boolean;
lastChecked: number;
}

View file

@ -1,5 +1,5 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
@ -382,6 +382,10 @@ class BitcoinApi implements AbstractBitcoinApi {
}
public startHealthChecks(): void {};
public getHealthStatus() {
return [];
}
}
export default BitcoinApi;

View file

@ -1,7 +1,7 @@
import config from '../../config';
import axios, { AxiosResponse } from 'axios';
import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
@ -10,6 +10,7 @@ interface FailoverHost {
host: string,
rtts: number[],
rtt: number,
timedOut?: boolean,
failures: number,
latestHeight?: number,
socket?: boolean,
@ -17,6 +18,7 @@ interface FailoverHost {
unreachable?: boolean,
preferred?: boolean,
checked: boolean,
lastChecked?: number,
}
class FailoverRouter {
@ -108,14 +110,20 @@ class FailoverRouter {
host.rtts = [];
host.rtt = Infinity;
}
host.timedOut = false;
} catch (e) {
host.outOfSync = true;
host.unreachable = true;
host.rtts = [];
host.rtt = Infinity;
if (isAxiosError(e) && (e.code === 'ECONNABORTED' || e.code === 'ETIMEDOUT')) {
host.timedOut = true;
} else {
host.timedOut = false;
}
}
host.checked = true;
host.lastChecked = Date.now();
// switch if the current host is out of sync or significantly slower than the next best alternative
const rankOrder = this.sortHosts();
@ -143,7 +151,7 @@ class FailoverRouter {
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅'));
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : (host.timedOut ? ' ⌛️💥 ' : ' - ')} ${!host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅')} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
private updateFallback(): FailoverHost[] {
@ -157,7 +165,7 @@ class FailoverRouter {
}
// sort hosts by connection quality, and update default fallback
private sortHosts(): FailoverHost[] {
public sortHosts(): FailoverHost[] {
// sort by connection quality
return this.hosts.slice().sort((a, b) => {
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
@ -342,6 +350,24 @@ class ElectrsApi implements AbstractBitcoinApi {
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
public getHealthStatus(): HealthCheckHost[] {
if (config.MEMPOOL.OFFICIAL) {
return this.failoverRouter.sortHosts().map(host => ({
host: host.host,
active: host === this.failoverRouter.activeHost,
rtt: host.rtt,
latestHeight: host.latestHeight || 0,
socket: !!host.socket,
outOfSync: !!host.outOfSync,
unreachable: !!host.unreachable,
checked: !!host.checked,
lastChecked: host.lastChecked || 0,
}));
} else {
return [];
}
}
}
export default ElectrsApi;

View file

@ -26,6 +26,7 @@ import mempool from './mempool';
import statistics from './statistics/statistics';
import accelerationCosts from './acceleration';
import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@ -39,6 +40,7 @@ const wantable = [
'mempool-blocks',
'live-2h-chart',
'stats',
'tomahawk',
];
class WebsocketHandler {
@ -123,7 +125,7 @@ class WebsocketHandler {
for (const sub of wantable) {
const key = `want-${sub}`;
const wants = parsedMessage.data.includes(sub);
if (wants && client['wants'] && !client[key]) {
if (wants && !client[key]) {
wantNow[key] = true;
}
client[key] = wants;
@ -147,6 +149,10 @@ class WebsocketHandler {
response['da'] = this.socketData['da'];
}
if (wantNow['want-tomahawk']) {
response['tomahawk'] = JSON.stringify(bitcoinApi.getHealthStatus());
}
if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx'];
@ -546,6 +552,10 @@ class WebsocketHandler {
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
}
if (client['want-tomahawk']) {
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
}
if (client['track-mempool-tx']) {
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
if (tx) {
@ -909,6 +919,10 @@ class WebsocketHandler {
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
}
if (client['want-tomahawk']) {
response['tomahawk'] = getCachedResponse('tomahawk', bitcoinApi.getHealthStatus());
}
if (client['track-tx']) {
const trackTxid = client['track-tx'];
if (trackTxid && confirmedTxids[trackTxid]) {

View file

@ -5,6 +5,7 @@ const configFromFile = require(
interface IConfig {
MEMPOOL: {
ENABLED: boolean;
OFFICIAL: boolean;
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
@ -103,6 +104,7 @@ interface IConfig {
PASSWORD: string;
TIMEOUT: number;
PID_DIR: string;
POOL_SIZE: number;
};
SYSLOG: {
ENABLED: boolean;
@ -161,6 +163,7 @@ interface IConfig {
const defaults: IConfig = {
'MEMPOOL': {
'ENABLED': true,
'OFFICIAL': false,
'NETWORK': 'mainnet',
'BACKEND': 'none',
'HTTP_PORT': 8999,
@ -240,6 +243,7 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 180000,
'PID_DIR': '',
'POOL_SIZE': 100,
},
'SYSLOG': {
'ENABLED': true,

View file

@ -21,7 +21,7 @@ import { execSync } from 'child_process';
database: config.DATABASE.DATABASE,
user: config.DATABASE.USERNAME,
password: config.DATABASE.PASSWORD,
connectionLimit: 10,
connectionLimit: config.DATABASE.POOL_SIZE,
supportBigNumbers: true,
timezone: '+00:00',
};

View file

@ -155,11 +155,17 @@ class Server {
}
if (Common.isLiquid()) {
try {
icons.loadIcons();
} catch (e) {
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
}
const refreshIcons = () => {
try {
icons.loadIcons();
} catch (e) {
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
}
};
// Run once on startup.
refreshIcons();
// Matches crontab refresh interval for asset db.
setInterval(refreshIcons, 3600_000);
}
priceUpdater.$run();

View file

@ -3,6 +3,7 @@
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"ENABLED": __MEMPOOL_ENABLED__,
"OFFICIAL": __MEMPOOL_OFFICIAL__,
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
@ -79,7 +80,8 @@
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__",
"TIMEOUT": __DATABASE_TIMEOUT__,
"PID_DIR": "__DATABASE_PID_DIR__"
"PID_DIR": "__DATABASE_PID_DIR__",
"POOL_SIZE": __DATABASE_POOL_SIZE__
},
"SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__,

View file

@ -4,6 +4,7 @@
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
__MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
@ -81,6 +82,7 @@ __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
__DATABASE_POOL_SIZE__=${DATABASE_POOL_SIZE:=100}
# SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
@ -158,6 +160,7 @@ mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
@ -230,6 +233,7 @@ sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
sed -i "s!__DATABASE_POOL_SIZE__!${__DATABASE_POOL_SIZE__}!g" mempool-config.json
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json

View file

@ -1,7 +1,7 @@
#!/bin/sh
#backend
cp ./docker/backend/* ./backend/
cp -r ./docker/backend/* ./backend/
#geoip-data
mkdir -p ./backend/GeoIP/
@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/
sed -i "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
sed -i "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i "s/user nobody;//g" ./frontend/nginx.conf
sed -i "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf
sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View file

@ -6,6 +6,7 @@ import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.com
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
const browserWindow = window || {};
// @ts-ignore
@ -26,6 +27,14 @@ let routes: Routes = [
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
@ -61,6 +70,14 @@ let routes: Routes = [
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
@ -88,6 +105,14 @@ let routes: Routes = [
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'preview',
children: [
@ -168,6 +193,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
@ -195,6 +228,14 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'preview',
children: [

View file

@ -0,0 +1,24 @@
<div class="frame {{ screenSize }}" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="heading">
<app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<h3 i18n="addresses.balance">Balances</h3>
<div class="spacer"></div>
</div>
<table class="table table-borderless table-striped table-fixed">
<tr>
<th class="address" i18n="addresses.total">Total</th>
<th class="btc"><app-amount [satoshis]="balance" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></th>
<th class="fiat"><app-fiat [value]="balance"></app-fiat></th>
</tr>
<tr *ngFor="let address of page">
<td class="address">
<app-truncate [text]="address" [lastChars]="8" [link]="['/address/' | relativeUrl, address]" [external]="true"></app-truncate>
</td>
<td class="btc"><app-amount [satoshis]="addresses[address]" [digitsInfo]="digitsInfo" [noFiat]="true"></app-amount></td>
<td class="fiat"><app-fiat [value]="addresses[address]"></app-fiat></td>
</tr>
</table>
<div *ngIf="addressStrings.length > itemsPerPage" class="pagination">
<ngb-pagination class="pagination-container float-right" [collectionSize]="addressStrings.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="pageIndex" (pageChange)="pageChange(pageIndex)" [boundaryLinks]="false" [ellipses]="false"></ngb-pagination>
</div>
</div>

View file

@ -0,0 +1,101 @@
.frame {
position: relative;
background: #24273e;
padding: 0.5rem;
height: calc(100% + 60px);
}
.heading {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
& > * {
flex-basis: 0;
flex-grow: 1;
}
h3 {
text-align: center;
margin: 0 1em;
}
}
.pagination {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
}
.table {
margin-top: 0.5em;
td, th {
padding: 0.15rem 0.5rem;
&.address {
width: auto;
}
&.btc {
width: 140px;
text-align: right;
}
&.fiat {
width: 142px;
text-align: right;
}
}
tr {
border-collapse: collapse;
&:first-child {
border-bottom: solid 1px white;
td, th {
padding-bottom: 0.3rem;
}
}
&:nth-child(2) {
td, th {
padding-top: 0.3rem;
}
}
&:nth-child(even) {
background: #181b2d;
}
}
@media (min-width: 528px) {
td, th {
&.btc {
width: 160px;
}
&.fiat {
width: 140px;
}
}
}
@media (min-width: 576px) {
td, th {
&.btc {
width: 170px;
}
&.fiat {
width: 140px;
}
}
}
@media (min-width: 992px) {
td, th {
&.btc {
width: 210px;
}
&.fiat {
width: 140px;
}
}
}
}

View file

@ -0,0 +1,212 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } 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, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-address-group',
templateUrl: './address-group.component.html',
styleUrls: ['./address-group.component.scss']
})
export class AddressGroupComponent implements OnInit, OnDestroy {
network = '';
balance = 0;
confirmed = 0;
mempool = 0;
addresses: { [address: string]: number | null };
addressStrings: string[] = [];
addressInfo: { [address: string]: AddressInformation | null };
seenTxs: { [txid: string ]: boolean } = {};
isLoadingAddress = true;
error: any;
mainSubscription: Subscription;
wsSubscription: Subscription;
page: string[] = [];
pageIndex: number = 1;
itemsPerPage: number = 10;
screenSize: 'lg' | 'md' | 'sm' = 'lg';
digitsInfo: string = '1.8-8';
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private stateService: StateService,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
this.onResize();
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.websocketService.want(['blocks']);
this.mainSubscription = this.route.queryParamMap
.pipe(
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.addresses = {};
this.addressInfo = {};
this.balance = 0;
this.addressStrings = params.get('addresses').split(',').map(address => {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
return address.toLowerCase();
} else {
return address;
}
});
return forkJoin(this.addressStrings.map(address => {
const getLiquidInfo = ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address));
return forkJoin([
of(address),
this.electrsApiService.getAddress$(address),
(getLiquidInfo ? this.apiService.validateAddress$(address) : of(null)),
]);
}));
}),
catchError(e => {
this.error = e;
return of([]);
})
).subscribe((addresses) => {
for (const addressData of addresses) {
const address = addressData[0];
const addressBalance = addressData[1] as Address;
if (addressBalance) {
this.addresses[address] = addressBalance.chain_stats.funded_txo_sum
+ addressBalance.mempool_stats.funded_txo_sum
- addressBalance.chain_stats.spent_txo_sum
- addressBalance.mempool_stats.spent_txo_sum;
this.balance += this.addresses[address];
this.confirmed += (addressBalance.chain_stats.funded_txo_sum - addressBalance.chain_stats.spent_txo_sum);
}
this.addressInfo[address] = addressData[2] ? addressData[2] as AddressInformation : null;
}
this.websocketService.startTrackAddresses(this.addressStrings);
this.isLoadingAddress = false;
this.pageChange(this.pageIndex);
});
this.wsSubscription = this.stateService.multiAddressTransactions$.subscribe(update => {
for (const address of Object.keys(update)) {
for (const tx of update[address].mempool) {
this.addTransaction(tx, false, false);
}
for (const tx of update[address].confirmed) {
this.addTransaction(tx, true, false);
}
for (const tx of update[address].removed) {
this.removeTransaction(tx, tx.status.confirmed);
}
}
});
}
pageChange(index): void {
this.page = this.addressStrings.slice((index - 1) * this.itemsPerPage, index * this.itemsPerPage);
}
addTransaction(transaction: Transaction, confirmed = false, playSound: boolean = true): boolean {
if (this.seenTxs[transaction.txid]) {
this.removeTransaction(transaction, false);
}
this.seenTxs[transaction.txid] = true;
let balance = 0;
transaction.vin.forEach((vin) => {
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
this.addresses[vin?.prevout?.scriptpubkey_address] -= vin.prevout.value;
balance -= vin.prevout.value;
this.balance -= vin.prevout.value;
if (confirmed) {
this.confirmed -= vin.prevout.value;
}
}
});
transaction.vout.forEach((vout) => {
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
this.addresses[vout?.scriptpubkey_address] += vout.value;
balance += vout.value;
this.balance += vout.value;
if (confirmed) {
this.confirmed += vout.value;
}
}
});
if (playSound) {
if (balance > 0) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
}
return true;
}
removeTransaction(transaction: Transaction, confirmed = false): boolean {
transaction.vin.forEach((vin) => {
if (this.addressStrings.includes(vin?.prevout?.scriptpubkey_address)) {
this.addresses[vin?.prevout?.scriptpubkey_address] += vin.prevout.value;
this.balance += vin.prevout.value;
if (confirmed) {
this.confirmed += vin.prevout.value;
}
}
});
transaction.vout.forEach((vout) => {
if (this.addressStrings.includes(vout?.scriptpubkey_address)) {
this.addresses[vout?.scriptpubkey_address] -= vout.value;
this.balance -= vout.value;
if (confirmed) {
this.confirmed -= vout.value;
}
}
});
return true;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.screenSize = 'lg';
this.digitsInfo = '1.8-8';
} else if (window.innerWidth >= 528) {
this.screenSize = 'md';
this.digitsInfo = '1.4-4';
} else {
this.screenSize = 'sm';
this.digitsInfo = '1.2-2';
}
const newItemsPerPage = Math.floor((window.innerHeight - 150) / 30);
if (newItemsPerPage !== this.itemsPerPage) {
this.itemsPerPage = newItemsPerPage;
this.pageIndex = 1;
this.pageChange(this.pageIndex);
}
}
ngOnDestroy(): void {
this.mainSubscription.unsubscribe();
this.wsSubscription.unsubscribe();
this.websocketService.stopTrackingAddresses();
}
}

View file

@ -56,7 +56,7 @@
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
</ng-container>
</div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
{{ block.extras.pool.name}}</a>

View file

@ -166,7 +166,7 @@
opacity: 1;
}
.hide {
opacity: 0;
opacity: 0.4;
pointer-events : none;
}

View file

@ -1,4 +1,4 @@
<footer class="footer">
<footer class="footer" [class.inline-footer]="inline">
<div class="container-xl">
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
<div class="col d-none d-sm-block">

View file

@ -6,6 +6,12 @@
background-color: #1d1f31;
box-shadow: 15px 15px 15px 15px #000;
z-index: 10;
&.inline-footer {
position: relative;
bottom: unset;
top: -44px;
}
}
.sub-text {

View file

@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@ -23,6 +23,8 @@ interface MempoolInfoData {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FooterComponent implements OnInit {
@Input() inline = false;
mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>;
vBytesPerSecondLimit = 1667;

View file

@ -1,7 +1,7 @@
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
<div class="flashing">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
<div class="flashing" *ngIf="(mempoolBlocks$ | async) as mempoolBlocks">
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks" let-i="index" [ngForTrackBy]="trackByFn">
<div
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
class="spotlight-bottom"

View file

@ -1,9 +1,9 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { take, map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@ -86,7 +86,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
public stateService: StateService,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
private location: Location
private location: Location,
) { }
enabledMiningInfoIfNeeded(url) {
@ -129,50 +129,44 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
})
);
this.mempoolBlocks$ = merge(
of(true),
fromEvent(window, 'resize')
)
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
if (!mempoolBlocks.length) {
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
}
return mempoolBlocks;
}),
)
])),
map(([lastBlock, mempoolBlocks]) => {
mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
});
this.mempoolBlocks$ = combineLatest([
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
if (!mempoolBlocks.length) {
return [{ index: 0, blockSize: 0, blockVSize: 0, feeRange: [0, 0], medianFee: 0, nTx: 0, totalFees: 0 }];
}
return mempoolBlocks;
}),
)
]).pipe(
map(([lastBlock, mempoolBlocks]) => {
mempoolBlocks.forEach((block, i) => {
block.index = this.blockIndex + i;
block.height = lastBlock.height + i + 1;
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
});
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
this.now = Date.now();
this.now = Date.now();
this.updateMempoolBlockStyles();
this.calculateTransactionPosition();
return this.mempoolBlocks;
}),
tap(() => {
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
this.cd.markForCheck();
}
})
);
this.updateMempoolBlockStyles();
this.calculateTransactionPosition();
return this.mempoolBlocks;
}),
tap(() => {
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
}
})
);
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
.pipe(

View file

@ -0,0 +1,36 @@
<div class="tomahawk">
<div class="links">
<span>Monitoring</span>
<a [routerLink]='"/nodes"'>Nodes</a>
</div>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngIf="(hosts$ | async) as hosts">
<div class="status-panel">
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
<tbody>
<tr>
<th class="rank"></th>
<th class="flag"></th>
<th class="host">Host</th>
<th class="updated">Last checked</th>
<th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th>
<th class="height">Height</th>
</tr>
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
<td class="rank">{{ i + 1 }}</td>
<td class="flag">{{ host.active ? '⭐️' : host.flag }}</td>
<td class="host">{{ host.link }}</td>
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '')) }}</td>
</tr>
</tbody>
</table>
</div>
</ng-container>
</div>

View file

@ -0,0 +1,75 @@
.tomahawk {
.links {
text-align: right;
margin-inline-end: 1em;
a, span {
margin-left: 1em;
}
}
.status-panel {
max-width: 720px;
margin: auto;
padding: 1em;
background: #24273e;
}
.status-table {
width: 100%;
td, th {
padding: 0.25em;
&.rank, &.flag {
width: 28px;
text-align: right;
}
&.updated {
display: none;
width: 130px;
text-align: right;
white-space: pre-wrap;
}
&.rtt, &.height {
width: 92px;
text-align: right;
}
&.only-small {
display: table-cell;
&.rtt {
width: 60px;
}
}
&.only-large {
display: none;
}
&.height {
padding-right: 0.5em;
}
&.host {
width: auto;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 576px) {
&.rank, &.flag {
width: 32px;
}
&.updated {
display: table-cell;
}
&.rtt, &.height {
width: 96px;
}
&.only-small {
display: none;
}
&.only-large {
display: table-cell;
}
}
}
}
}

View file

@ -0,0 +1,84 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { Observable, Subject, map } from 'rxjs';
import { StateService } from '../../services/state.service';
import { HealthCheckHost } from '../../interfaces/websocket.interface';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-server-health',
templateUrl: './server-health.component.html',
styleUrls: ['./server-health.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServerHealthComponent implements OnInit {
hosts$: Observable<HealthCheckHost[]>;
tip$: Subject<number>;
interval: number;
now: number = Date.now();
constructor(
private websocketService: WebsocketService,
private stateService: StateService,
private cd: ChangeDetectorRef,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.hosts$ = this.stateService.serverHealth$.pipe(
map((hosts) => {
const subpath = window.location.pathname.slice(0, -11);
for (const host of hosts) {
let statusUrl = '';
let linkHost = '';
if (host.socket) {
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
linkHost = window.location.hostname + subpath;
} else {
const hostUrl = new URL(host.host);
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
linkHost = hostUrl.hostname + subpath;
}
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
host.link = linkHost;
host.flag = this.parseFlag(host.host);
}
return hosts;
})
);
this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
this.interval = window.setInterval(() => {
this.now = Date.now();
this.cd.markForCheck();
}, 1000);
}
trackByFn(index: number, host: HealthCheckHost): string {
return host.host;
}
getLastUpdateSeconds(host: HealthCheckHost): string {
if (host.lastChecked) {
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
} else {
return '~';
}
}
private parseFlag(host: string): string {
if (host.includes('.fra.')) {
return '🇩🇪';
} else if (host.includes('.tk7.')) {
return '🇯🇵';
} else if (host.includes('.fmt.')) {
return '🇺🇸';
} else if (host.includes('.va1.')) {
return '🇺🇸';
} else {
return '';
}
}
}

View file

@ -0,0 +1,16 @@
<div class="tomahawk">
<div class="links">
<a [routerLink]='"/monitoring"'>Monitoring</a>
<span>Nodes</span>
</div>
<app-start [showLoadingIndicator]="true"></app-start>
<app-footer [inline]="true"></app-footer>
<ng-container *ngFor="let host of hosts; trackBy: trackByFn">
<h5 [id]="host.host" class="hostLink">
<a [href]="'https://' + host.link">{{ host.link }}</a>
</h5>
<iframe class="mempoolStatus" [src]="host.statusPage"></iframe>
</ng-container>
</div>

View file

@ -0,0 +1,22 @@
.tomahawk {
.links {
text-align: right;
margin-inline-end: 1em;
a, span {
margin-left: 1em;
}
}
.mempoolStatus {
width: 100%;
height: 270px;
border: none;
}
.hostLink {
text-align: center;
margin: auto;
margin-top: 1em;
}
}

View file

@ -0,0 +1,80 @@
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { Observable, Subject, Subscription, map, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { HealthCheckHost } from '../../interfaces/websocket.interface';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-server-status',
templateUrl: './server-status.component.html',
styleUrls: ['./server-status.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ServerStatusComponent implements OnInit, OnDestroy {
tip$: Subject<number>;
hosts: HealthCheckHost[] = [];
hostSubscription: Subscription;
constructor(
private websocketService: WebsocketService,
private stateService: StateService,
private cd: ChangeDetectorRef,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.hostSubscription = this.stateService.serverHealth$.pipe(
map((hosts) => {
const subpath = window.location.pathname.slice(0, -6);
for (const host of hosts) {
let statusUrl = '';
let linkHost = '';
if (host.socket) {
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
linkHost = window.location.hostname + subpath;
} else {
const hostUrl = new URL(host.host);
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
linkHost = hostUrl.hostname + subpath;
}
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
host.link = linkHost;
}
return hosts;
}),
tap((hosts) => {
if (this.hosts.length !== hosts.length) {
this.hosts = hosts.sort((a,b) => {
const aParts = (a.host?.split('.') || []).reverse();
const bParts = (b.host?.split('.') || []).reverse();
let i = 0;
while (i < Math.max(aParts.length, bParts.length)) {
if (aParts[i] && !bParts[i]) {
return 1;
} else if (bParts[i] && !aParts[i]) {
return -1;
} else if (aParts[i] !== bParts[i]) {
return aParts[i].localeCompare(bParts[i]);
}
i++;
}
return 0;
});
}
this.cd.markForCheck();
})
).subscribe();
this.tip$ = this.stateService.chainTip$;
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
}
trackByFn(index: number, host: HealthCheckHost): string {
return host.host;
}
ngOnDestroy(): void {
this.hosts = [];
this.hostSubscription.unsubscribe();
}
}

View file

@ -1,3 +1,7 @@
<div id="enterprise-cta-desktop">
<p>Get higher API limits with Mempool Enterprise®</p>
<a class="btn btn-small btn-purple" href="/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
</div>
<div *ngFor="let item of tabData">
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ))">{{ item.title }}</p>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>

View file

@ -11,3 +11,22 @@ a {
display: block;
margin: 5px 0;
}
#enterprise-cta-desktop {
text-align: center;
padding: 20px;
margin: 20px 20px 20px 0;
background-color: #1d1f31;
border-radius: 12px;
}
#enterprise-cta-desktop p {
margin: 0 auto 16px auto;
color: #fff;
font-weight: 400;
}
#enterprise-cta-desktop a {
display: inline-block;
}

View file

@ -39,6 +39,14 @@
<div class="doc-content">
<div id="enterprise-cta-mobile" *ngIf="showMobileEnterpriseUpsell">
<p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
<div class="button-group">
<a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
<a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
</div>
</div>
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>

View file

@ -315,6 +315,41 @@ h3 {
margin-bottom: 0;
}
#enterprise-cta-mobile {
padding: 20px;
background-color: #1d1f31;
border-radius: 0.25rem;
text-align: center;
position: fixed;
z-index: 100;
left: 30px;
width: calc(100% - 60px);
bottom: 70px;
display: none;
border: 3px solid #533180;
}
#enterprise-cta-mobile p {
font-size: 16px;
display: inline-block;
margin: 0 auto;
}
#enterprise-cta-mobile a {
padding: 4px 8px;
font-size: 16px;
margin: 15px 5px 5px 5px;
}
#enterprise-cta-mobile .btn-secondary:hover {
background-color: #2d3348;
border-color: #2d3348;
}
#enterprise-cta-mobile .no-line-break {
white-space: nowrap;
}
@media (max-width: 992px) {
h3 {
@ -373,6 +408,10 @@ h3 {
#disclaimer table {
display: none;
}
#enterprise-cta-mobile {
display: initial;
}
}
@media (min-width: 992px) {

View file

@ -30,6 +30,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
officialMempoolInstance: boolean;
auditEnabled: boolean;
mobileViewport: boolean = false;
showMobileEnterpriseUpsell: boolean = true;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;

View file

@ -1,3 +1,4 @@
import { SafeResourceUrl } from '@angular/platform-browser';
import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
@ -120,4 +121,19 @@ export interface Recommendedfees {
hourFee: number;
minimumFee: number;
economyFee: number;
}
export interface HealthCheckHost {
host: string;
active: boolean;
rtt: number;
latestHeight: number;
socket: boolean;
outOfSync: boolean;
unreachable: boolean;
checked: boolean;
lastChecked: number;
link?: string;
statusPage?: SafeResourceUrl;
flag?: string;
}

View file

@ -19,6 +19,8 @@ import { RecentPegsListComponent } from '../components/liquid-reserves-audit/rec
import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component';
import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component';
import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component';
import { ServerHealthComponent } from '../components/server-health/server-health.component';
import { ServerStatusComponent } from '../components/server-health/server-status.component';
const routes: Routes = [
{
@ -140,6 +142,19 @@ const routes: Routes = [
},
];
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
routes[0].children.push({
path: 'nodes',
data: { networks: ['bitcoin', 'liquid'] },
component: ServerHealthComponent
});
routes[0].children.push({
path: 'network',
data: { networks: ['bitcoin', 'liquid'] },
component: ServerStatusComponent
});
}
@NgModule({
imports: [
RouterModule.forChild(routes)

View file

@ -6,10 +6,13 @@ import { SharedModule } from './shared/shared.module';
import { StartComponent } from './components/start/start.component';
import { AddressComponent } from './components/address/address.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { CalculatorComponent } from './components/calculator/calculator.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
import { ServerHealthComponent } from './components/server-health/server-health.component';
import { ServerStatusComponent } from './components/server-health/server-status.component';
const browserWindow = window || {};
// @ts-ignore
@ -96,6 +99,19 @@ const routes: Routes = [
}
];
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
routes[0].children.push({
path: 'monitoring',
data: { networks: ['bitcoin', 'liquid'] },
component: ServerHealthComponent
});
routes[0].children.push({
path: 'nodes',
data: { networks: ['bitcoin', 'liquid'] },
component: ServerStatusComponent
});
}
@NgModule({
imports: [
RouterModule.forChild(routes)

View file

@ -1,14 +1,13 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionCompressed, TransactionStripped } from '../interfaces/websocket.interface';
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ApiService } from './api.service';
import { ActiveFilter } from '../shared/filters.utils';
export interface MarkBlockState {
@ -119,6 +118,7 @@ export class StateService {
mempoolTransactions$ = new Subject<Transaction>();
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
mempoolRemovedTransactions$ = new Subject<Transaction>();
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
@ -129,6 +129,7 @@ export class StateService {
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
chainTip$ = new ReplaySubject<number>(-1);
serverHealth$ = new Subject<HealthCheckHost[]>();
live2Chart$ = new Subject<OptimizedMempoolStats>();

View file

@ -32,6 +32,7 @@ export class WebsocketService {
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
private isTrackingRbfSummary = false;
private isTrackingAddress: string | false = false;
private isTrackingAddresses: string[] | false = false;
private trackingMempoolBlock: number;
private latestGitCommit = '';
private onlineCheckTimeout: number;
@ -126,6 +127,9 @@ export class WebsocketService {
if (this.isTrackingAddress) {
this.startTrackAddress(this.isTrackingAddress);
}
if (this.isTrackingAddresses) {
this.startTrackAddresses(this.isTrackingAddresses);
}
this.stateService.connectionState$.next(2);
}
@ -175,6 +179,16 @@ export class WebsocketService {
this.isTrackingAddress = false;
}
startTrackAddresses(addresses: string[]) {
this.websocketSubject.next({ 'track-addresses': addresses });
this.isTrackingAddresses = addresses;
}
stopTrackingAddresses() {
this.websocketSubject.next({ 'track-addresses': [] });
this.isTrackingAddresses = false;
}
startTrackAsset(asset: string) {
this.websocketSubject.next({ 'track-asset': asset });
}
@ -374,6 +388,10 @@ export class WebsocketService {
});
}
if (response['multi-address-transactions']) {
this.stateService.multiAddressTransactions$.next(response['multi-address-transactions']);
}
if (response['block-transactions']) {
response['block-transactions'].forEach((addressTransaction: Transaction) => {
this.stateService.blockTransactions$.next(addressTransaction);
@ -415,6 +433,10 @@ export class WebsocketService {
this.stateService.previousRetarget$.next(response.previousRetarget);
}
if (response['tomahawk']) {
this.stateService.serverHealth$.next(response['tomahawk']);
}
if (response['git-commit']) {
this.stateService.backendInfo$.next(response['git-commit']);
}

View file

@ -1,6 +1,6 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link">
<a [routerLink]="link" class="truncate-link" [target]="external ? '_blank' : ''">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
</a>
</ng-container>

View file

@ -9,6 +9,7 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@a
export class TruncateComponent {
@Input() text: string;
@Input() link: any = null;
@Input() external: boolean = false;
@Input() lastChars: number = 4;
@Input() maxWidth: number = null;
@Input() inline: boolean = false;

View file

@ -46,6 +46,7 @@ import { BlockOverviewGraphComponent } from '../components/block-overview-graph/
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
import { AddressComponent } from '../components/address/address.component';
import { AddressGroupComponent } from '../components/address-group/address-group.component';
import { SearchFormComponent } from '../components/search-form/search-form.component';
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
import { FooterComponent } from '../components/footer/footer.component';
@ -53,6 +54,8 @@ import { AssetComponent } from '../components/asset/asset.component';
import { AssetsComponent } from '../components/assets/assets.component';
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from '../components/status-view/status-view.component';
import { ServerHealthComponent } from '../components/server-health/server-health.component';
import { ServerStatusComponent } from '../components/server-health/server-status.component';
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
@ -145,12 +148,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BlockFiltersComponent,
TransactionsListComponent,
AddressComponent,
AddressGroupComponent,
SearchFormComponent,
AddressLabelsComponent,
FooterComponent,
AssetComponent,
AssetsComponent,
StatusViewComponent,
ServerHealthComponent,
ServerStatusComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,
@ -271,12 +277,15 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BlockFiltersComponent,
TransactionsListComponent,
AddressComponent,
AddressGroupComponent,
SearchFormComponent,
AddressLabelsComponent,
FooterComponent,
AssetComponent,
AssetsComponent,
StatusViewComponent,
ServerHealthComponent,
ServerStatusComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,

31
production/check Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env zsh
#for j in fmt va1 fra tk7;do for i in 1 2 3 4 5 6;do echo -n 20$i.$j: ;curl -i -s https://node20$i.$j.mempool.space/api/v1/services/accelerator/accelerations|head -1;done;done
check_mempoolspace_frontend_git_hash() {
echo curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/resources/config.js
echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space/resources/config.js|grep GIT_COMMIT_HASH|cut -d "'" -f2|cut -c1-8)
}
check_mempoolfoss_frontend_git_hash() {
echo -n $(curl -s "https://node${1}.${2}.mempool.space/resources/config.js"|grep GIT_COMMIT_HASH|cut -d "'" -f2|cut -c1-8)
}
check_mempoolspace_frontend_md5_hash() {
echo -n $(curl -s --connect-to "::node${1}.${2}.mempool.space:443" https://mempool.space|md5|cut -c1-8)
}
check_mempoolfoss_frontend_md5_hash() {
echo -n $(curl -s https://node${1}.${2}.mempool.space|md5|cut -c1-8)
}
for site in fmt va1 fra tk7;do
echo "${site}"
for node in 201 202 203 204 205 206 207 208 209 210 211 212 213 214;do
[ "${site}" = "fmt" ] && [ "${node}" -gt 206 ] && continue
[ "${site}" = "tk7" ] && [ "${node}" -gt 206 ] && continue
echo -n "node${node}.${site}: "
#check_mempoolspace_frontend_git_hash $node $site
#echo -n " "
check_mempoolspace_frontend_md5_hash $node $site
echo -n " "
check_mempoolfoss_frontend_git_hash $node $site
echo -n " "
check_mempoolfoss_frontend_md5_hash $node $site
echo
done
done

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "bisq",
"BACKEND": "esplora",
"HTTP_PORT": 8996,

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "liquid",
"BACKEND": "esplora",
"HTTP_PORT": 8998,

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "liquid",
"BACKEND": "esplora",
"HTTP_PORT": 8994,

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"ENABLED": false,
"NETWORK": "mainnet",
"BACKEND": "esplora",

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "mainnet",
"BACKEND": "esplora",
"HTTP_PORT": 8999,
@ -21,7 +22,8 @@
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 12
"PRICE_UPDATES_PER_HOUR": 12,
"MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"ENABLED": false,
"NETWORK": "signet",
"BACKEND": "esplora",

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "signet",
"BACKEND": "esplora",
"HTTP_PORT": 8995,
@ -14,7 +15,8 @@
"POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
"ALLOW_UNREACHABLE": true,
"MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"ENABLED": false,
"NETWORK": "testnet",
"BACKEND": "esplora",

View file

@ -1,5 +1,6 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "testnet",
"BACKEND": "esplora",
"HTTP_PORT": 8997,
@ -14,7 +15,8 @@
"POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
"ALLOW_UNREACHABLE": true,
"MAX_TRACKED_ADDRESSES": 10
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"

View file

@ -8,5 +8,9 @@ proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inac
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m;
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m;
types_hash_max_size 4096;
proxy_buffer_size 8k;
types_hash_max_size 8192;
proxy_busy_buffers_size 256k;
proxy_buffer_size 128k;
proxy_buffers 4 256k;

View file

@ -0,0 +1,25 @@
BASE_HEIGHT=$(curl -sk https://node202.tk7.mempool.space/api/v1/blocks/tip/height)
IN_SYNC=true
echo "Base height (node202.tk7): $BASE_HEIGHT"
for LOCATION in fmt va1 fra tk7
do
for NODE in 201 202 203 204 205 206
do
NODE_HEIGHT=$(curl -sk https://node$NODE.$LOCATION.mempool.space/api/v1/blocks/tip/height)
echo $(echo node$NODE.$LOCATION.mempool.space) - $NODE_HEIGHT
if [ "$NODE_HEIGHT" -ne "$BASE_HEIGHT" ]; then
COUNT=$((BASE_HEIGHT-NODE_HEIGHT))
echo $(echo node$NODE.$LOCATION.mempool.space) is not in sync. delta: $COUNT
IN_SYNC=false
fi
done
done
if [ "$IN_SYNC" = false ]; then
echo "One or more servers are out of sync. Check the logs."
exit -1
else
echo "All servers are in sync."
fi