Merge branch 'master' into mononaut/fractional-fees

This commit is contained in:
wiz 2025-11-28 04:15:03 -06:00 committed by GitHub
commit 0ecce2801e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
299 changed files with 19628 additions and 19685 deletions

View file

@ -0,0 +1,145 @@
name: Backend Integration Tests with MariaDB
on:
pull_request:
types: [opened, review_requested, synchronize]
push:
branches:
- master
jobs:
backend-integration:
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
strategy:
matrix:
node: ["22.14.0"]
fail-fast: false
runs-on: ubuntu-latest
name: Backend Integration Tests - node ${{ matrix.node }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ matrix.node }}/integration
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
cache: 'npm'
cache-dependency-path: '${{ matrix.node }}/integration/backend/package-lock.json'
- name: Cache node modules
uses: actions/cache@v4
with:
path: ${{ matrix.node }}/integration/backend/node_modules
key: ${{ runner.os }}-backend-integration-node-${{ matrix.node }}-${{ hashFiles('${{ matrix.node }}/integration/backend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-backend-integration-node-${{ matrix.node }}-
${{ runner.os }}-backend-integration-
- name: Read rust-toolchain file from repository
id: gettoolchain
run: echo "::set-output name=toolchain::$(cat ./rust/gbt/rust-toolchain)"
working-directory: ${{ matrix.node }}/integration
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
${{ matrix.node }}/integration/rust/gbt/target/
key: ${{ runner.os }}-cargo-integration-${{ hashFiles('${{ matrix.node }}/integration/rust/gbt/**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-integration-
${{ runner.os }}-cargo-
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
- name: Install dependencies
run: npm ci
working-directory: ${{ matrix.node }}/integration/backend
- name: Build backend
run: npm run build
working-directory: ${{ matrix.node }}/integration/backend
- name: Verify config file exists
run: |
ls -la mempool-config.test.json
echo "Current directory: ${PWD}"
echo "Config file will be: ${{ github.workspace }}/${{ matrix.node }}/integration/backend/mempool-config.test.json"
test -f mempool-config.test.json || (echo "ERROR: Config file not found!" && exit 1)
working-directory: ${{ matrix.node }}/integration/backend
- name: Run integration tests (DB auto-starts via Jest)
run: |
echo "MEMPOOL_CONFIG_FILE=$MEMPOOL_CONFIG_FILE"
npm run test:integration
working-directory: ${{ matrix.node }}/integration/backend
env:
MEMPOOL_CONFIG_FILE: ${{ github.workspace }}/${{ matrix.node }}/integration/backend/mempool-config.test.json
- name: Start MariaDB for server test
run: docker compose -f docker-compose.test.yml up -d
working-directory: ${{ matrix.node }}/integration/backend
- name: Wait for MariaDB
run: |
echo "Waiting for MariaDB to be ready..."
for i in {1..30}; do
if docker compose -f docker-compose.test.yml exec -T db-test mysqladmin ping -h localhost -u mempool_test -pmempool_test --silent 2>/dev/null; then
echo "MariaDB is ready!"
break
fi
echo "Attempt $i/30..."
sleep 2
done
sleep 3
working-directory: ${{ matrix.node }}/integration/backend
- name: Start backend server and verify connectivity
run: |
# Start server in background
node dist/index.js &
SERVER_PID=$!
# Wait for server to start
echo "Waiting for server to start..."
sleep 10
# Check if server is still running
if ps -p $SERVER_PID > /dev/null 2>&1; then
echo "Server started successfully and connected to database!"
kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true
exit 0
else
echo "Server failed to start or exited prematurely"
exit 1
fi
working-directory: ${{ matrix.node }}/integration/backend
env:
MEMPOOL_CONFIG_FILE: ${{ github.workspace }}/${{ matrix.node }}/integration/backend/mempool-config.test.json
- name: Cleanup containers
if: always()
run: docker compose -f docker-compose.test.yml down -v
working-directory: ${{ matrix.node }}/integration/backend
- name: Display logs on failure
if: failure()
run: |
echo "=== MariaDB logs ==="
docker compose -f docker-compose.test.yml logs db-test || true
working-directory: ${{ matrix.node }}/integration/backend

View file

@ -10,12 +10,18 @@ on:
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
pull_request:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
jobs:
build:
# Run on tag pushes OR on PRs that have the "docker" label
if: |
github.event_name == 'push' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'docker'))
strategy:
matrix:
service:
@ -56,15 +62,20 @@ jobs:
sudo systemctl restart docker
sudo df -h | grep docker
- name: Set env variables
# Only for tag pushes: use the Git tag as TAG
- name: Set TAG from pushed tag
if: github.event_name == 'push'
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
SHA="${{ github.event.pull_request.head.sha }}"
else
SHA="${GITHUB_SHA}"
fi
echo "SHORT_SHA=${SHA:0:8}" >> $GITHUB_ENV
- name: Login to Docker for building
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
@ -72,6 +83,22 @@ jobs:
- name: Checkout project
uses: actions/checkout@v4
# For PRs: use package.json version + short sha as TAG
- name: Set TAG from service package.json for pull requests
if: github.event_name == 'pull_request'
run: |
if [ "${{ matrix.service }}" = "frontend" ]; then
VERSION=$(jq -r '.version' frontend/package.json)
else
VERSION=$(jq -r '.version' backend/package.json)
fi
echo "TAG=v${VERSION}-${SHORT_SHA}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
printf " SHORT_SHA: %s\n" "$SHORT_SHA"
- name: Init repo for Dockerization
run: docker/init.sh "$TAG"
@ -117,7 +144,8 @@ jobs:
tag-latest:
needs: build
if: ${{ needs.build.result == 'success' && !contains(github.ref_name, '-') }}
# Only for successful *tag pushes* and only for "plain" versions (no '-')
if: ${{ needs.build.result == 'success' && github.event_name == 'push' && !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
timeout-minutes: 30
name: Tag release build as latest

View file

@ -0,0 +1,20 @@
version: "3.7"
services:
db-test:
image: mariadb:10.5.21
environment:
MYSQL_DATABASE: "mempool_test"
MYSQL_USER: "mempool_test"
MYSQL_PASSWORD: "mempool_test"
MYSQL_ROOT_PASSWORD: "admin"
ports:
- "33306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "mempool_test", "-pmempool_test"]
interval: 5s
timeout: 5s
retries: 10
tmpfs:
- /var/lib/mysql

View file

@ -16,5 +16,9 @@ const config: Config.InitialOptions = {
setupFiles: [
"./testSetup.ts",
],
testPathIgnorePatterns: [
"/node_modules/",
"/__integration_tests__/",
],
}
export default config;

View file

@ -0,0 +1,21 @@
import type { Config } from "@jest/types"
const config: Config.InitialOptions = {
preset: "ts-jest",
testEnvironment: "node",
verbose: true,
automock: false,
collectCoverage: false,
coverageProvider: "v8",
testMatch: [
"**/__integration_tests__/**/*.test.ts"
],
globalSetup: "./jest.integration.setup.ts", // Start database before all tests
setupFiles: [
"./testSetup.integration.ts",
],
globalTeardown: "./jest.integration.teardown.ts", // Stop database after all tests
maxWorkers: 1, // Force sequential execution
}
export default config;

View file

@ -0,0 +1,72 @@
// Setup that runs BEFORE setupFiles
// This ensures MEMPOOL_CONFIG_FILE is set before any modules are loaded
import * as path from 'path';
import { execSync } from 'child_process';
// Set the config file path if not already set
if (!process.env.MEMPOOL_CONFIG_FILE) {
process.env.MEMPOOL_CONFIG_FILE = path.join(__dirname, 'mempool-config.test.json');
}
// Helper to get docker compose command (v1 or v2)
function getDockerComposeCmd(): string {
try {
execSync('docker compose version', { stdio: 'pipe' });
return 'docker compose';
} catch {
try {
execSync('docker-compose version', { stdio: 'pipe' });
return 'docker-compose';
} catch {
throw new Error('Neither "docker compose" nor "docker-compose" is available');
}
}
}
// Start the Docker test database container
module.exports = async () => {
// Skip if SKIP_DB_SETUP is set (e.g., when test-with-db.sh manages the database)
if (process.env.SKIP_DB_SETUP) {
console.log('Skipping database setup (managed externally)');
return;
}
console.log('Starting test database container...');
try {
const composeFile = path.join(__dirname, 'docker-compose.test.yml');
const dockerComposeCmd = getDockerComposeCmd();
// Start the container
execSync(`${dockerComposeCmd} -f "${composeFile}" up -d`, {
stdio: 'inherit',
cwd: __dirname
});
// Wait for database to be ready
console.log('Waiting for database to be ready...');
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
try {
execSync(`${dockerComposeCmd} -f "${composeFile}" exec -T db-test mysqladmin ping -h localhost -u mempool_test -pmempool_test --silent`, {
cwd: __dirname,
stdio: 'pipe'
});
console.log('Database is ready!');
break;
} catch (e) {
attempts++;
if (attempts >= maxAttempts) {
throw new Error('Database did not start in time');
}
// Wait 1 second before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
} catch (error) {
console.error('Failed to start test database:', error instanceof Error ? error.message : error);
throw error;
}
};

View file

@ -0,0 +1,102 @@
import DB from './src/database';
import logger from './src/logger';
import mempool from './src/api/mempool';
import { execSync } from 'child_process';
import * as path from 'path';
// Helper to get docker compose command (v1 or v2)
function getDockerComposeCmd(): string {
try {
execSync('docker compose version', { stdio: 'pipe' });
return 'docker compose';
} catch {
try {
execSync('docker-compose version', { stdio: 'pipe' });
return 'docker-compose';
} catch {
throw new Error('Neither "docker compose" nor "docker-compose" is available');
}
}
}
module.exports = async () => {
try {
// Final cleanup after all tests
const tables = [
'blocks_audits',
'blocks_summaries',
'blocks_prices',
'blocks_templates',
'cpfp_clusters',
'blocks',
'difficulty_adjustments',
'hashrates',
'prices',
'node_records',
'nodes_sockets',
'nodes',
'lightning_stats',
'transactions',
'elements_pegs',
'federation_txos',
'pools'
];
await DB.query('SET FOREIGN_KEY_CHECKS = 0');
for (const table of tables) {
try {
// Use 'silent' error logging to avoid noise for optional tables that don't exist
await DB.query(`TRUNCATE TABLE ${table}`, [], 'silent');
} catch (e) {
// Table might not exist - silently ignore
}
}
await DB.query('SET FOREIGN_KEY_CHECKS = 1');
logger.info('Integration tests cleanup completed');
// Close the database connection pool to prevent Jest from hanging
await DB.close();
logger.info('Database connection pool closed');
// Clean up singleton resources that have timers or sockets
mempool.destroy();
logger.info('Mempool resources cleaned up');
// Close logger's UDP socket last (after all logging is done)
logger.close();
// Stop and remove the Docker test database container
// Skip if SKIP_DB_TEARDOWN is set (e.g., when test-with-db.sh manages the database)
if (!process.env.SKIP_DB_TEARDOWN) {
try {
const composeFile = path.join(__dirname, 'docker-compose.test.yml');
const dockerComposeCmd = getDockerComposeCmd();
execSync(`${dockerComposeCmd} -f "${composeFile}" down -v`, {
stdio: 'inherit',
cwd: __dirname
});
console.log('Test database container stopped and removed');
} catch (error) {
console.error('Failed to stop Docker container:', error instanceof Error ? error.message : error);
}
} else {
console.log('Skipping Docker cleanup (managed externally)');
}
} catch (error) {
// Use console.error since logger might be closed
console.error('Failed to cleanup after integration tests:', error instanceof Error ? error.message : error);
} finally {
// Ensure we always try to close connections even if cleanup fails
try {
await DB.close();
mempool.destroy();
logger.close();
} catch (e) {
// Ignore errors on close
}
}
};

View file

@ -0,0 +1,163 @@
{
"MEMPOOL": {
"OFFICIAL": false,
"NETWORK": "mainnet",
"BACKEND": "none",
"ENABLED": true,
"HTTP_PORT": 8998,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CACHE_ENABLED": false,
"CLEAR_PROTECTION_MINUTES": 20,
"RECOMMENDED_FEE_PERCENTILE": 50,
"BLOCK_WEIGHT_UNITS": 4000000,
"INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 11000,
"BLOCKS_SUMMARIES_INDEXING": false,
"GOGGLES_INDEXING": false,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1,
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false,
"RUST_GBT": true,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1,
"MAX_TRACKED_ADDRESSES": 100,
"UNIX_SOCKET_PATH": ""
},
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 33306,
"SOCKET": "",
"DATABASE": "mempool_test",
"USERNAME": "mempool_test",
"PASSWORD": "mempool_test",
"TIMEOUT": 180000,
"PID_DIR": ""
},
"SYSLOG": {
"ENABLED": false,
"HOST": "127.0.0.1",
"PORT": 514,
"MIN_PRIORITY": "info",
"FACILITY": "local7"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
"MAXMIND": {
"ENABLED": false,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
},
"LIGHTNING": {
"ENABLED": false,
"BACKEND": "lnd",
"STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30,
"FORENSICS_INTERVAL": 43200,
"FORENSICS_RATE_LIMIT": 20
},
"LND": {
"TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
},
"CLIGHTNING": {
"SOCKET": "lightning-rpc"
},
"SOCKS5PROXY": {
"ENABLED": false,
"USE_ONION": true,
"HOST": "127.0.0.1",
"PORT": 9050,
"USERNAME": "",
"PASSWORD": ""
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
"LIQUID_API": "https://liquid.network/api/v1",
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1"
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": {
"ENABLED": false,
"AUDIT": false,
"AUDIT_START_HEIGHT": 774000,
"STATISTICS": false,
"STATISTICS_START_TIME": 1481932800,
"SERVERS": []
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": {
"ENABLED": false,
"PAID": false,
"API_KEY": ""
}
}

8593
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,8 @@
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
"test": "./node_modules/.bin/jest --coverage",
"test:ci": "CI=true ./node_modules/.bin/jest --coverage",
"test:integration": "./node_modules/.bin/jest --config=jest.integration.config.ts --runInBand --forceExit",
"test:with-db": "bash ./scripts/test-with-db.sh",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
@ -57,15 +59,19 @@
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/jest": "^30.0.0",
"@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"jest": "^30.0.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"ts-jest": "^29.4.5",
"ts-node": "^10.9.1"
},
"overrides": {
"js-yaml": "^4.1.1",
"glob": "^11.1.0"
}
}

View file

@ -0,0 +1,40 @@
#!/bin/bash
# Script to debug integration tests
# Usage: ./scripts/debug-integration-tests.sh [test-file-pattern]
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Get the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BACKEND_DIR="$( cd "$SCRIPT_DIR/.." && pwd )"
cd "$BACKEND_DIR"
# Check if MariaDB is running
if ! docker-compose -f docker-compose.test.yml ps | grep -q "Up"; then
echo -e "${YELLOW}Starting MariaDB container...${NC}"
docker-compose -f docker-compose.test.yml up -d
# Wait for MariaDB
echo -e "${YELLOW}Waiting for MariaDB...${NC}"
sleep 5
fi
# Run tests with pattern if provided
if [ -n "$1" ]; then
echo -e "${GREEN}Running tests matching: $1${NC}"
MEMPOOL_CONFIG_FILE="$BACKEND_DIR/mempool-config.test.json" npx jest --config=jest.integration.config.ts --runInBand --verbose "$1"
else
echo -e "${GREEN}Running all integration tests${NC}"
MEMPOOL_CONFIG_FILE="$BACKEND_DIR/mempool-config.test.json" npm run test:integration
fi
echo -e "${GREEN}Tests completed${NC}"

111
backend/scripts/test-with-db.sh Executable file
View file

@ -0,0 +1,111 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Starting integration tests with MariaDB...${NC}"
# Get the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BACKEND_DIR="$( cd "$SCRIPT_DIR/.." && pwd )"
cd "$BACKEND_DIR"
# Detect docker compose command (v1 or v2)
if docker compose version &> /dev/null; then
DOCKER_COMPOSE="docker compose"
elif docker-compose version &> /dev/null; then
DOCKER_COMPOSE="docker-compose"
else
echo -e "${RED}Error: Neither 'docker compose' nor 'docker-compose' is available${NC}"
exit 1
fi
echo -e "${YELLOW}Using: ${DOCKER_COMPOSE}${NC}"
# Function to cleanup on exit
cleanup() {
echo -e "\n${YELLOW}Cleaning up...${NC}"
# Kill the backend server if it's running
if [ ! -z "$SERVER_PID" ]; then
kill $SERVER_PID 2>/dev/null || true
fi
# Stop containers and remove volumes, but don't fail on network errors
$DOCKER_COMPOSE -f docker-compose.test.yml down -v 2>&1 | grep -v "Resource is still in use" || true
}
# Set trap to cleanup on exit
trap cleanup EXIT INT TERM
# Stop any existing test containers
echo -e "${YELLOW}Stopping any existing test containers...${NC}"
$DOCKER_COMPOSE -f docker-compose.test.yml down -v 2>/dev/null || true
# Start MariaDB container
echo -e "${GREEN}Starting MariaDB container...${NC}"
$DOCKER_COMPOSE -f docker-compose.test.yml up -d
# Wait for MariaDB to be ready
echo -e "${YELLOW}Waiting for MariaDB to be ready...${NC}"
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if $DOCKER_COMPOSE -f docker-compose.test.yml exec -T db-test mysqladmin ping -h localhost -u mempool_test -pmempool_test --silent 2>/dev/null; then
echo -e "${GREEN}MariaDB is ready!${NC}"
break
fi
attempt=$((attempt + 1))
echo -e "${YELLOW}Attempt $attempt/$max_attempts - waiting for MariaDB...${NC}"
sleep 2
done
if [ $attempt -eq $max_attempts ]; then
echo -e "${RED}MariaDB did not start in time${NC}"
exit 1
fi
# Additional wait to ensure MariaDB is fully initialized
sleep 3
# Build the backend
echo -e "${GREEN}Building backend...${NC}"
npm run build
# Run integration tests with absolute path to config
# SKIP_DB_SETUP=1 and SKIP_DB_TEARDOWN=1 tell Jest that we're managing the database lifecycle
echo -e "${GREEN}Running integration tests...${NC}"
export MEMPOOL_CONFIG_FILE="$BACKEND_DIR/mempool-config.test.json"
export SKIP_DB_SETUP=1
export SKIP_DB_TEARDOWN=1
npm run test:integration
# Start the backend server in the background
# MEMPOOL_CONFIG_FILE is already exported above
echo -e "${GREEN}Starting backend server...${NC}"
node dist/index.js &
SERVER_PID=$!
# Wait for server to start and verify connection
echo -e "${YELLOW}Waiting for server to start and connect to database...${NC}"
sleep 10
# Check if server is still running
if ps -p $SERVER_PID > /dev/null 2>&1; then
echo -e "${GREEN}Server started successfully and connected to database!${NC}"
# Kill the server (it will be in the cleanup function too, but do it here as well)
kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true
SERVER_PID=""
else
echo -e "${RED}Server failed to start or exited prematurely${NC}"
exit 1
fi
echo -e "${GREEN}All tests passed successfully!${NC}"

View file

@ -0,0 +1,134 @@
import BlocksRepository from '../repositories/BlocksRepository';
import { setupTestDatabase, waitForDatabase, cleanupTestData, insertTestPool, insertTestBlock } from './test-helpers';
describe('BlocksRepository Integration Tests', () => {
let defaultPoolId: number;
beforeAll(async () => {
await waitForDatabase();
await setupTestDatabase();
}, 120000);
beforeEach(async () => {
await cleanupTestData();
// Create a default pool for all blocks
defaultPoolId = await insertTestPool({
name: 'Unknown',
slug: 'unknown',
addresses: '[]',
regexes: '[]'
});
});
afterAll(async () => {
await cleanupTestData();
});
test('should insert and retrieve a block', async () => {
const blockHash = '00000000000000000001a0e3e9b2e6d6e8a4b0e9b2e6d6e8a4b0e9b2e6d6e8a4';
const height = 800000;
await insertTestBlock({
height: height,
hash: blockHash,
blockTimestamp: new Date('2023-07-16T00:00:00Z'),
size: 1500000,
weight: 3999000,
tx_count: 3000,
difficulty: 53911173001054.59,
poolId: defaultPoolId
});
const block = await BlocksRepository.$getBlockByHeight(height);
expect(block).toBeDefined();
expect(block!.height).toBe(height);
expect(block!.id).toBe(blockHash);
});
test('should get block by hash', async () => {
const blockHash = '00000000000000000002b0e3e9b2e6d6e8a4b0e9b2e6d6e8a4b0e9b2e6d6e8a4';
const height = 800001;
await insertTestBlock({
height: height,
hash: blockHash,
tx_count: 2500,
poolId: defaultPoolId
});
const block = await BlocksRepository.$getBlockByHash(blockHash);
expect(block).toBeDefined();
expect(block!.id).toBe(blockHash);
expect(block!.height).toBe(height);
});
test('should handle non-existent block', async () => {
const block = await BlocksRepository.$getBlockByHeight(999999);
expect(block).toBeNull();
});
test('should check for missing blocks in range', async () => {
// Insert blocks with a gap
await insertTestBlock({
height: 800100,
hash: '0000000000000000000100000000000000000000000000000000000000000001',
poolId: defaultPoolId
});
await insertTestBlock({
height: 800102,
hash: '0000000000000000000100000000000000000000000000000000000000000003',
poolId: defaultPoolId
});
const missingBlocks = await BlocksRepository.$getMissingBlocksBetweenHeights(800100, 800102);
expect(missingBlocks).toContain(800101);
});
test('should get latest block height', async () => {
await insertTestBlock({
height: 800200,
hash: '0000000000000000000200000000000000000000000000000000000000000001',
poolId: defaultPoolId
});
await insertTestBlock({
height: 800201,
hash: '0000000000000000000200000000000000000000000000000000000000000002',
poolId: defaultPoolId
});
const height = await BlocksRepository.$mostRecentBlockHeight();
expect(height).toBe(800201);
});
test('should handle block with pool association', async () => {
// Insert a pool and get its auto-generated ID
const testPoolId = await insertTestPool({
name: 'Test Pool',
slug: 'test-pool',
addresses: '[]',
regexes: '[]'
});
const blockHash = '0000000000000000000300000000000000000000000000000000000000000001';
await insertTestBlock({
height: 800300,
hash: blockHash,
poolId: testPoolId
});
const block = await BlocksRepository.$getBlockByHash(blockHash);
expect(block).toBeDefined();
expect(block).not.toBeNull();
// The pool should be populated with the test pool's data
if (block && block.extras?.pool) {
expect(block.extras.pool.name).toBe('Test Pool');
expect(block.extras.pool.slug).toBe('test-pool');
}
});
});

View file

@ -0,0 +1,45 @@
import DB from '../database';
import config from '../config';
import { setupTestDatabase, waitForDatabase, getTestDatabaseConfig } from './test-helpers';
describe('Database Connection Integration Tests', () => {
beforeAll(async () => {
// Wait for database to be ready
await waitForDatabase();
}, 60000);
test('should connect to the test database', async () => {
const dbConfig = getTestDatabaseConfig();
expect(dbConfig.enabled).toBe(true);
expect(dbConfig.database).toBe('mempool_test');
expect(dbConfig.port).toBe(33306);
});
test('should execute a simple query', async () => {
const [result] = await DB.query<any>('SELECT 1 as value');
expect(result).toHaveLength(1);
expect(result[0].value).toBe(1);
});
test('should execute a query with parameters', async () => {
const [result] = await DB.query<any>('SELECT ? as sum', [42]);
expect(result).toHaveLength(1);
expect(result[0].sum).toBe(42);
});
test('should check database connection', async () => {
await expect(DB.checkDbConnection()).resolves.not.toThrow();
});
test('should handle query timeout configuration', async () => {
expect(config.DATABASE.TIMEOUT).toBeGreaterThan(0);
});
test('should have correct database configuration', () => {
expect(config.DATABASE.HOST).toBe('127.0.0.1');
expect(config.DATABASE.USERNAME).toBe('mempool_test');
expect(config.DATABASE.PASSWORD).toBe('mempool_test');
expect(config.DATABASE.DATABASE).toBe('mempool_test');
});
});

View file

@ -0,0 +1,97 @@
import DB from '../database';
import { setupTestDatabase, waitForDatabase } from './test-helpers';
describe('Database Migration Integration Tests', () => {
beforeAll(async () => {
await waitForDatabase();
await setupTestDatabase();
}, 120000);
test('should create state table', async () => {
const [result] = await DB.query<any>(
`SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'mempool_test'
AND table_name = 'state'`
);
expect(result[0].count).toBe(1);
});
test('should have schema version in state table', async () => {
const [result] = await DB.query<any>("SELECT number FROM state WHERE name = 'schema_version'");
expect(result).toHaveLength(1);
expect(result[0].number).toBeGreaterThan(0);
});
test('should create blocks table', async () => {
const [result] = await DB.query<any>(
`SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'mempool_test'
AND table_name = 'blocks'`
);
expect(result[0].count).toBe(1);
});
test('should create pools table', async () => {
const [result] = await DB.query<any>(
`SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'mempool_test'
AND table_name = 'pools'`
);
expect(result[0].count).toBe(1);
});
test('should create hashrates table', async () => {
const [result] = await DB.query<any>(
`SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'mempool_test'
AND table_name = 'hashrates'`
);
expect(result[0].count).toBe(1);
});
test('should create prices table', async () => {
const [result] = await DB.query<any>(
`SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'mempool_test'
AND table_name = 'prices'`
);
expect(result[0].count).toBe(1);
});
test('blocks table should have required columns', async () => {
const [columns] = await DB.query<any>(
`SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'mempool_test'
AND TABLE_NAME = 'blocks'`
);
const columnNames = columns.map((col: any) => col.COLUMN_NAME);
expect(columnNames).toContain('height');
expect(columnNames).toContain('hash');
expect(columnNames).toContain('blockTimestamp');
expect(columnNames).toContain('size');
expect(columnNames).toContain('weight');
expect(columnNames).toContain('tx_count');
});
test('pools table should have required columns', async () => {
const [columns] = await DB.query<any>(
`SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'mempool_test'
AND TABLE_NAME = 'pools'`
);
const columnNames = columns.map((col: any) => col.COLUMN_NAME);
expect(columnNames).toContain('id');
expect(columnNames).toContain('name');
expect(columnNames).toContain('slug');
});
});

View file

@ -0,0 +1,122 @@
import PoolsRepository from '../repositories/PoolsRepository';
import { setupTestDatabase, waitForDatabase, cleanupTestData, insertTestPool } from './test-helpers';
describe('PoolsRepository Integration Tests', () => {
beforeAll(async () => {
await waitForDatabase();
await setupTestDatabase();
}, 120000);
beforeEach(async () => {
await cleanupTestData();
});
afterAll(async () => {
await cleanupTestData();
});
test('should insert and retrieve a pool', async () => {
const poolData = {
name: 'Foundry USA',
slug: 'foundryusa',
link: 'https://foundrydigital.com',
addresses: JSON.stringify(['bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj']),
regexes: JSON.stringify(['/Foundry USA Pool/'])
};
const poolId = await insertTestPool(poolData);
expect(poolId).toBeGreaterThan(0);
const pools = await PoolsRepository.$getPools();
const insertedPool = pools.find(p => p.id === poolId);
expect(insertedPool).toBeDefined();
expect(insertedPool?.name).toBe(poolData.name);
expect(insertedPool?.slug).toBe(poolData.slug);
});
test('should get pool by slug', async () => {
await insertTestPool({
name: 'AntPool',
slug: 'antpool',
link: 'https://antpool.com'
});
const pool = await PoolsRepository.$getPool('antpool');
expect(pool).toBeDefined();
expect(pool!.name).toBe('AntPool');
expect(pool!.slug).toBe('antpool');
});
test('should get all pools', async () => {
await insertTestPool({
name: 'Pool 1',
slug: 'pool-1'
});
await insertTestPool({
name: 'Pool 2',
slug: 'pool-2'
});
await insertTestPool({
name: 'Pool 3',
slug: 'pool-3'
});
const pools = await PoolsRepository.$getPools();
expect(pools.length).toBeGreaterThanOrEqual(3);
});
test('should handle pool with addresses', async () => {
const addresses = ['bc1qtest1', 'bc1qtest2', '3TestAddress'];
await insertTestPool({
name: 'Multi Address Pool',
slug: 'multi-address-pool',
addresses: JSON.stringify(addresses)
});
const pool = await PoolsRepository.$getPool('multi-address-pool', false);
expect(pool).toBeDefined();
const poolAddresses = JSON.parse(pool!.addresses);
expect(poolAddresses).toHaveLength(3);
expect(poolAddresses).toContain('bc1qtest1');
});
test('should handle pool with regexes', async () => {
const regexes = ['/Pool Name/', '/Alternative Name/'];
await insertTestPool({
name: 'Regex Pool',
slug: 'regex-pool',
regexes: JSON.stringify(regexes)
});
const pool = await PoolsRepository.$getPool('regex-pool', false);
expect(pool).toBeDefined();
const poolRegexes = JSON.parse(pool!.regexes);
expect(poolRegexes).toHaveLength(2);
expect(poolRegexes[0]).toBe('/Pool Name/');
});
test('should handle non-existent pool', async () => {
const pool = await PoolsRepository.$getPool('non-existent-pool-slug');
expect(pool).toBeNull();
});
test('should update pool information', async () => {
const poolDbId = await insertTestPool({
name: 'Original Pool Name',
slug: 'original-pool'
});
// Update the pool name
await PoolsRepository.$renameMiningPool(poolDbId, 'updated-pool', 'Updated Pool Name');
const pool = await PoolsRepository.$getPool('updated-pool');
expect(pool).toBeDefined();
expect(pool!.name).toBe('Updated Pool Name');
});
});

View file

@ -0,0 +1,190 @@
import DB from '../database';
import config from '../config';
import logger from '../logger';
import databaseMigration from '../api/database-migration';
/**
* Initialize the test database with schema migrations
*/
export async function setupTestDatabase(): Promise<void> {
try {
await DB.checkDbConnection();
await databaseMigration.$initializeOrMigrateDatabase();
} catch (error) {
logger.err('Failed to setup test database: ' + (error instanceof Error ? error.message : error));
throw error;
}
}
/**
* Clean up all data from test tables (but preserve schema)
* This runs between each test to ensure isolation
*/
export async function cleanupTestData(): Promise<void> {
// Order matters: delete child tables before parent tables
const tables = [
'blocks_audits',
'blocks_summaries',
'blocks_prices',
'blocks_templates',
'cpfp_clusters',
'blocks', // blocks references pools
'difficulty_adjustments',
'hashrates',
'prices',
'node_records',
'nodes_sockets',
'nodes',
'lightning_stats',
'transactions',
'elements_pegs',
'federation_txos',
'pools'
];
try {
// Disable foreign key checks temporarily for faster cleanup
await DB.query('SET FOREIGN_KEY_CHECKS = 0');
for (const table of tables) {
try {
// Use 'silent' error logging to avoid noise for optional tables that don't exist
await DB.query(`TRUNCATE TABLE ${table}`, [], 'silent');
} catch (e) {
// Table might not exist, that's okay for optional tables
// Silently ignore - no need to log since these are expected for optional features
}
}
// Re-enable foreign key checks
await DB.query('SET FOREIGN_KEY_CHECKS = 1');
} catch (error) {
// Try to re-enable foreign keys even if cleanup failed
try {
await DB.query('SET FOREIGN_KEY_CHECKS = 1');
} catch (e) {
// Ignore
}
logger.err('Failed to cleanup test data: ' + (error instanceof Error ? error.message : error));
throw error;
}
}
/**
* Wait for database to be ready
*/
export async function waitForDatabase(maxRetries = 30, retryInterval = 1000): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await DB.query('SELECT 1');
logger.info('Database is ready');
return;
} catch (error) {
logger.debug(`Waiting for database... attempt ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
throw new Error('Database did not become ready in time');
}
/**
* Get database configuration for tests
*/
export function getTestDatabaseConfig() {
return {
host: config.DATABASE.HOST,
port: config.DATABASE.PORT,
database: config.DATABASE.DATABASE,
username: config.DATABASE.USERNAME,
enabled: config.DATABASE.ENABLED
};
}
/**
* Insert a test pool into the database
*/
export async function insertTestPool(poolData: {
id?: number;
name: string;
link?: string;
slug: string;
addresses?: string;
regexes?: string;
}) {
const [result] = await DB.query<any>(
`INSERT INTO pools (unique_id, name, link, slug, addresses, regexes)
VALUES (?, ?, ?, ?, ?, ?)`,
[
poolData.id || -1,
poolData.name,
poolData.link || '',
poolData.slug,
poolData.addresses || '[]',
poolData.regexes || '[]'
]
);
return result.insertId;
}
/**
* Insert a test block into the database
*/
export async function insertTestBlock(blockData: {
height: number;
hash: string;
blockTimestamp?: Date;
size?: number;
weight?: number;
tx_count?: number;
difficulty?: number;
poolId?: number | null;
}) {
const timestamp = blockData.blockTimestamp || new Date();
const size = blockData.size || 1000000;
const weight = blockData.weight || 4000000;
const txCount = blockData.tx_count || 2000;
await DB.query(
`INSERT INTO blocks (
height, hash, blockTimestamp, size, weight, tx_count,
difficulty, pool_id, version, bits, nonce, merkle_root,
previous_block_hash, median_timestamp, stale,
fees, fee_span, median_fee,
avg_tx_size, total_inputs, total_outputs, total_output_amt,
segwit_total_txs, segwit_total_size, segwit_total_weight,
header, utxoset_change
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
blockData.height,
blockData.hash,
timestamp,
size,
weight,
txCount,
blockData.difficulty || 1.0,
blockData.poolId !== undefined ? blockData.poolId : null,
0x20000000,
0x1d00ffff,
0,
'0000000000000000000000000000000000000000000000000000000000000000',
'0000000000000000000000000000000000000000000000000000000000000000',
timestamp,
0, // stale = false
// Required fields with defaults
50000000, // fees (in sats)
JSON.stringify([0, 0, 0, 0, 0, 0, 0]), // fee_span (JSON array)
10000, // median_fee (in sats)
size / txCount, // avg_tx_size
txCount * 2, // total_inputs (estimate)
txCount * 2, // total_outputs (estimate)
2100000000000000, // total_output_amt (21M BTC in sats, estimate)
txCount, // segwit_total_txs (assume all segwit for test)
size, // segwit_total_size
weight, // segwit_total_weight
'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', // header (160 chars)
0 // utxoset_change
]
);
}

View file

@ -1,52 +1,55 @@
[
{
"txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019",
"vout": 4217,
"prevout": {
"scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33",
"value": 106
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01",
"02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d"
],
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "6a023a29",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29",
"scriptpubkey_type": "op_return",
"value": 0
{
"txid": "f859a4e6692c3f15a279449eac191ecb9d17d2d6c003bbae9a55e5da07147eab",
"version": 1,
"locktime": 0,
"size": 170,
"weight": 371,
"fee": 11586,
"vin": [
{
"is_coinbase": false,
"prevout": {
"value": 11586,
"scriptpubkey": "512088939b42f2ed113dd4756055b179cd784c2cfa52cad29046411c28335aa2055c",
"scriptpubkey_address": "bc1p3zfekshja5gnm4r4vp2mz7wd0pxze7jjetffq3jprs5rxk4zq4wqesykl2",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 88939b42f2ed113dd4756055b179cd784c2cfa52cad29046411c28335aa2055c",
"scriptpubkey_type": "v1_p2tr"
},
"scriptsig": "",
"scriptsig_asm": "",
"sequence": 4294967295,
"txid": "9b93dab94a3334b6119f75d107da6bcef9435fa52ec6c0de14aa8a094794551a",
"vout": 0,
"witness": [
"6840b6fa27a00ba001cc92797ce4f3ab7b7a32c21d1fce49e893b42e506bd92e8db187966a84ef799915cf671334cc59779915b192bfb66b2afcf384bb61d0f4",
"500049276d20616e20616e6e6578212041726520796f7520616e20616e6e65783f00"
],
"inner_redeemscript_asm": "",
"inner_witnessscript_asm": ""
}
],
"vout": [
{
"value": 0,
"scriptpubkey": "6a05616e6e6578",
"scriptpubkey_address": "",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_5 616e6e6578",
"scriptpubkey_type": "op_return"
}
],
"status": {
"confirmed": true,
"block_height": 896088,
"block_hash": "0000000000000000000148370c6ad0eceb23d0de54ea86a362679cee7fcd3f4a",
"block_time": 1746868490
},
{
"scriptpubkey": "6a036d7648",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648",
"scriptpubkey_type": "op_return",
"value": 0
}
],
"size": 186,
"weight": 420,
"sigops": 1,
"fee": 106,
"status": {
"confirmed": true,
"block_height": 836361,
"block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b",
"block_time": 1711448028
"order": 2877166599,
"vsize": 93,
"adjustedVsize": 92.75,
"sigops": 0,
"feePerVsize": 124.91644204851752,
"adjustedFeePerVsize": 124.91644204851752,
"effectiveFeePerVsize": 124.91644204851752
}
}
]

View file

@ -11,6 +11,7 @@ class AccelerationRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/:txid', this.$getAcceleratorAcceleration.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
@ -23,6 +24,19 @@ class AccelerationRoutes {
res.status(200).send(Object.values(accelerations));
}
private async $getAcceleratorAcceleration(req: Request, res: Response): Promise<void> {
if (req.params.txid) {
const acceleration = await AccelerationRepository.$getAccelerationInfoForTxid(req.params.txid);
if (acceleration) {
res.status(200).send(acceleration);
} else {
res.status(404).send('Acceleration not found');
}
} else {
res.status(400).send('txid is required');
}
}
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> {
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
res.status(200).send(history.map(accel => ({

View file

@ -24,15 +24,15 @@ const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
const DUST_RELAY_TX_FEE = 3;
const MAX_OP_RETURN_RELAY = 83;
const DEFAULT_PERMIT_BAREMULTISIG = true;
const MAX_TX_LEGACY_SIGOPS = 2_500 * 4; // witness-adjusted sigops
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
: '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
static _isLiquid = config.MEMPOOL.NETWORK === 'liquid' || config.MEMPOOL.NETWORK === 'liquidtestnet';
static isLiquid(): boolean {
return this._isLiquid;
return config?.MEMPOOL?.NETWORK === 'liquid' || config?.MEMPOOL?.NETWORK === 'liquidtestnet';
}
static median(numbers: number[]) {
@ -225,6 +225,11 @@ export class Common {
return true;
}
// legacy sigops
if (this.isNonStandardLegacySigops(tx, height)) {
return true;
}
// input validation
for (const vin of tx.vin) {
if (vin.is_coinbase) {
@ -286,6 +291,7 @@ export class Common {
// output validation
let opreturnCount = 0;
let opreturnBytes = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
@ -309,10 +315,7 @@ export class Common {
}
} else if (vout.scriptpubkey_type === 'op_return') {
opreturnCount++;
if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
// over default datacarrier limit
return true;
}
opreturnBytes += vout.scriptpubkey.length / 2;
}
// dust
// (we could probably hardcode this for the different output types...)
@ -334,9 +337,11 @@ export class Common {
}
}
// multi-op-return
if (opreturnCount > 1) {
return true;
// op_return
if (opreturnCount > 0) {
if (!this.isStandardOpReturn(opreturnBytes, opreturnCount, height)) {
return true;
}
}
// TODO: non-mandatory-script-verify-flag
@ -429,6 +434,49 @@ export class Common {
return false;
}
// OP_RETURN size & count limits were lifted in v28.3/v29.2/v30.0
static OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
static MAX_DATACARRIER_BYTES = 83;
static isStandardOpReturn(bytes: number, outputs: number,height?: number): boolean {
if (
(height == null || (
this.OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height >= this.OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
)) // limits lifted
|| // OR
(bytes <= this.MAX_DATACARRIER_BYTES && outputs <= 1) // below old limits
) {
return true;
}
return false;
}
// New legacy sigops limit started to be enforced in v30.0
static LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
static isNonStandardLegacySigops(tx: TransactionExtended, height?: number): boolean {
if (
height == null || (
this.LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height >= this.LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
)
) {
if (!transactionUtils.checkSigopsBIP54(tx, MAX_TX_LEGACY_SIGOPS)) {
return true;
}
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;

View file

@ -21,8 +21,7 @@ class Mempool {
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 mempoolInfo: IBitcoinApi.MempoolInfo;
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
@ -44,11 +43,36 @@ class Mempool {
private timer = new Date().getTime();
private missingTxCount = 0;
private mainLoopTimeout: number = 120000;
private txPerSecondInterval: NodeJS.Timeout | null = null;
public limitGBT = config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE && config.MEMPOOL.LIMIT_GBT;
constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000);
// Initialize mempoolInfo here to avoid circular dependency issues
// Use config directly instead of Common.isLiquid() to break circular dependency
const isLiquid = config.MEMPOOL.NETWORK === 'liquid' || config.MEMPOOL.NETWORK === 'liquidtestnet';
this.mempoolInfo = {
loaded: false,
size: 0,
bytes: 0,
usage: 0,
total_fee: 0,
maxmempool: 300000000,
mempoolminfee: isLiquid ? 0.00000100 : 0.00001000,
minrelaytxfee: isLiquid ? 0.00000100 : 0.00001000
};
this.txPerSecondInterval = setInterval(this.updateTxPerSecond.bind(this), 1000);
}
/**
* Cleanup resources (timers, etc.)
* This should only be called when shutting down or in test teardown
*/
public destroy(): void {
if (this.txPerSecondInterval) {
clearInterval(this.txPerSecondInterval);
this.txPerSecondInterval = null;
}
}
/**

View file

@ -145,6 +145,9 @@ class TransactionUtils {
return str;
}
/**
* Calculate the witness-adjusted sigops cost of an asm script
*/
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
if (!script?.length) {
return 0;
@ -213,6 +216,41 @@ class TransactionUtils {
return sigops;
}
/**
* see https://github.com/bitcoin/bitcoin/blob/25c45bb0d0bd6618ec9296a1a43605657124e5de/src/policy/policy.cpp#L166-L193
* returns true if the transactions is permitted under bip54 sigops rules
*
* "Unlike the existing block wide sigop limit which counts sigops present in the block
* itself (including the scriptPubKey which is not executed until spending later), BIP54
* counts sigops in the block where they are potentially executed (only).
* This means sigops in the spent scriptPubKey count toward the limit.
* `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
* or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
* The GetSigOpCount call on the previous scriptPubKey counts both bare and P2SH sigops."
*/
public checkSigopsBIP54(tx: TransactionExtended, limit): boolean {
let sigops = 0;
for (const input of tx.vin) {
if (input.scriptsig_asm) {
sigops += this.countScriptSigops(input.scriptsig_asm);
}
if (input.prevout) {
// P2SH redeem script
if (input.prevout.scriptpubkey_type === 'p2sh' && input.inner_redeemscript_asm) {
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
} else {
// prevout scriptpubkey
sigops += this.countScriptSigops(input.prevout.scriptpubkey_asm);
}
}
if (sigops > limit) {
return false;
}
}
return true;
}
// returns the most significant 4 bytes of the txid as an integer
public txidToOrdering(txid: string): number {
return parseInt(

View file

@ -180,6 +180,19 @@ import { execSync } from 'child_process';
}
return this.pool;
}
/**
* Close the database connection pool
* This should only be called when the application is shutting down
* or at the end of test suites
*/
public async close(): Promise<void> {
if (this.pool !== null) {
await this.pool.end();
this.pool = null;
logger.debug('Database connection pool closed');
}
}
}
export default new DB();

View file

@ -142,9 +142,9 @@ class Server {
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count,X-Mempool-Auth');
next();
})
.use(express.urlencoded({ extended: true }))
.use(express.text({ type: ['text/plain', 'application/base64'] }))
.use(express.json())
.use(express.urlencoded({ extended: true, limit: '10mb' }))
.use(express.text({ type: ['text/plain', 'application/base64'], limit: '10mb' }))
.use(express.json({ limit: '10mb' }))
;
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {

View file

@ -67,6 +67,8 @@ class Logger {
}
}
this.client = dgram.createSocket('udp4');
// Unref the socket so it doesn't prevent Node.js from exiting
this.client.unref();
this.network = this.getNetwork();
}
@ -153,6 +155,20 @@ class Logger {
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds;
}
/**
* Close the UDP socket used for syslog
* This should only be called when shutting down or in test teardown
*/
public close(): void {
if (this.client) {
// Unref allows Node.js to exit even if the socket is open
this.client.unref();
this.client.close(() => {
// Socket closed callback
});
}
}
}
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';

View file

@ -60,6 +60,32 @@ class AccelerationRepository {
}
}
public async $getAccelerationInfoForTxid(txid: string): Promise<PublicAcceleration | null> {
const [rows] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations
JOIN pools on pools.unique_id = accelerations.pool
WHERE txid = ?
`, [txid]) as RowDataPacket[][];
if (rows?.length) {
const row = rows[0];
return {
txid: row.txid,
height: row.height,
added: row.requested_timestamp || row.block_timestamp,
pool: {
id: row.id,
slug: row.slug,
name: row.name,
},
effective_vsize: row.effective_vsize,
effective_fee: row.effective_fee,
boost_rate: row.boost_rate,
boost_cost: row.boost_cost,
};
}
return null;
}
public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise<PublicAcceleration[]> {
if (!interval || !['24h', '3d', '1w', '1m'].includes(interval)) {
interval = '1m';

View file

@ -272,7 +272,11 @@ class BlocksRepository {
* Get all block height that have not been indexed between [startHeight, endHeight]
*/
public async $getMissingBlocksBetweenHeights(startHeight: number, endHeight: number): Promise<number[]> {
if (startHeight < endHeight) {
// Ensure startHeight is the lower value and endHeight is the higher value
const minHeight = Math.min(startHeight, endHeight);
const maxHeight = Math.max(startHeight, endHeight);
if (minHeight === maxHeight) {
return [];
}
@ -280,13 +284,13 @@ class BlocksRepository {
const [rows]: any[] = await DB.query(`
SELECT height
FROM blocks
WHERE height <= ? AND height >= ? AND stale = 0
ORDER BY height DESC;
`, [startHeight, endHeight]);
WHERE height >= ? AND height <= ? AND stale = 0
ORDER BY height ASC;
`, [minHeight, maxHeight]);
const indexedBlockHeights: number[] = [];
rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
const seekedBlocks: number[] = Array.from(Array(maxHeight - minHeight + 1).keys(), n => n + minHeight);
const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
return missingBlocksHeights;

View file

@ -0,0 +1,14 @@
// Integration test setup - uses real implementations, not mocks
//
// Note: We don't mock ./mempool-config.json here because:
// 1. The MEMPOOL_CONFIG_FILE env var points to mempool-config.test.json
// 2. config.ts will load that file via require() if env var is set
// 3. If we mock it, we interfere with the actual config loading
//
// Don't mock these for integration tests - we want real implementations:
// - logger.ts (need real logging)
// - config.ts (need real config from mempool-config.test.json)
// - rbf-cache.ts (might be used by repositories)
// - mempool.ts (might be used by repositories)
// - memory-cache.ts (might be used by repositories)

View file

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of October 31, 2025.
Signed: AaronDewes

View file

@ -25,7 +25,7 @@
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
"handle": "Metaplanet"
}
},
{

View file

@ -1,7 +1,7 @@
// source: chrisp_68 @ https://stackoverflow.com/questions/50525143/how-do-you-reliably-wait-for-page-idle-in-cypress-io-test
export class PageIdleDetector
{
defaultOptions: Object = { timeout: 60000 };
defaultOptions: object = { timeout: 60000 };
public WaitForPageToBeIdle(): void
{
@ -11,7 +11,7 @@ export class PageIdleDetector
this.WaitForAnimationsToStop();
}
public WaitForPageToLoad(options: Object = this.defaultOptions): void
public WaitForPageToLoad(options: object = this.defaultOptions): void
{
cy.document(options).should((myDocument: any) =>
{
@ -19,7 +19,7 @@ export class PageIdleDetector
});
}
public WaitForAngularRequestsToComplete(options: Object = this.defaultOptions): void
public WaitForAngularRequestsToComplete(options: object = this.defaultOptions): void
{
cy.window(options).should((myWindow: any) =>
{
@ -30,7 +30,7 @@ export class PageIdleDetector
});
}
public WaitForAngularDigestCycleToComplete(options: Object = this.defaultOptions): void
public WaitForAngularDigestCycleToComplete(options: object = this.defaultOptions): void
{
cy.window(options).should((myWindow: any) =>
{
@ -41,7 +41,7 @@ export class PageIdleDetector
});
}
public WaitForAnimationsToStop(options: Object = this.defaultOptions): void
public WaitForAnimationsToStop(options: object = this.defaultOptions): void
{
cy.get(":animated", options).should("not.exist");
}

92
frontend/eslint.config.js Normal file
View file

@ -0,0 +1,92 @@
import js from '@eslint/js';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
// Flat config migrated from legacy .eslintrc
export default [
{
ignores: [
'node_modules/**',
'dist/**',
'src/resources/**',
// Keep parity with legacy .eslintignore
'frontend/**',
'server.run.js',
],
},
js.configs.recommended,
// Node globals for local JS utility scripts in this package
{
// Apply to all JS files in this package (including nested ones)
files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
languageOptions: {
globals: {
require: 'readonly',
module: 'readonly',
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
Buffer: 'readonly',
console: 'readonly',
InitWally: 'readonly',
document: 'readonly',
window: 'readonly',
},
},
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
// Make common browser globals available in TS as well
globals: {
document: 'readonly',
window: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
// Adjust base recommended rules for TypeScript (matches legacy extends: plugin:@typescript-eslint/eslint-recommended)
...tsPlugin.configs['eslint-recommended']?.overrides?.[0]?.rules,
// Start from @typescript-eslint's recommended rules
...tsPlugin.configs.recommended.rules,
// Project-specific rules migrated from .eslintrc
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-namespace': 'warn',
'@typescript-eslint/no-this-alias': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-expressions': 'warn',
'@typescript-eslint/no-require-imports': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'no-case-declarations': 'warn',
'no-console': 'warn',
'no-constant-condition': 'warn',
'no-dupe-else-if': 'warn',
'no-empty': 'warn',
'no-extra-boolean-cast': 'warn',
'no-prototype-builtins': 'warn',
'no-self-assign': 'warn',
'no-useless-catch': 'warn',
'no-var': 'warn',
'prefer-const': 'warn',
'prefer-rest-params': 'warn',
'quotes': ['warn', 'single', { allowTemplateLiterals: true }],
'semi': 'warn',
'curly': ['warn', 'all'],
'eqeqeq': 'warn',
'no-trailing-spaces': 'warn',
},
},
];

View file

@ -29,7 +29,7 @@ if (configContent && configContent.CUSTOMIZATION) {
try {
customConfig = readConfig(configContent.CUSTOMIZATION);
customConfigContent = JSON.parse(customConfig);
} catch (e) {
} catch {
console.log(`failed to load customization config from ${configContent.CUSTOMIZATION}`);
}
}
@ -54,7 +54,7 @@ try {
throw new Error(e);
}
for (setting in configContent) {
for (const setting in configContent) {
settings.push({
key: setting,
value: configContent[setting]
@ -98,7 +98,7 @@ function readConfig(path) {
try {
const currentConfig = fs.readFileSync(path).toString().trim();
return currentConfig;
} catch (e) {
} catch {
return false;
}
}
@ -136,13 +136,11 @@ writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
if (currentConfig && currentConfig === newConfig) {
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
return;
} else if (!currentConfig) {
console.log(`${GENERATED_CONFIG_FILE_NAME} file not found, creating new config file`);
console.log('CONFIG: ', newConfig);
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
console.log(`${GENERATED_CONFIG_FILE_NAME} file saved`);
return;
} else {
console.log(`Configuration changes detected, updating ${GENERATED_CONFIG_FILE_NAME} file`);
console.log('OLD CONFIG: ', currentConfig);

27610
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -32,13 +32,10 @@
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
"generate-config": "node generate-config.js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js",
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
"test": "npm run ng -- test",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
@ -60,63 +57,59 @@
"cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",
"@angular/animations": "^17.3.1",
"@angular/cli": "^17.3.1",
"@angular/common": "^17.3.1",
"@angular/compiler": "^17.3.1",
"@angular/core": "^17.3.1",
"@angular/forms": "^17.3.1",
"@angular/localize": "^17.3.1",
"@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.3.1",
"@angular/platform-server": "^17.3.1",
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@angular-devkit/build-angular": "^20.3.12",
"@angular/animations": "^20.3.14",
"@angular/cli": "^20.3.12",
"@angular/common": "^20.3.14",
"@angular/compiler": "^20.3.14",
"@angular/core": "^20.3.14",
"@angular/forms": "^20.3.14",
"@angular/localize": "^20.3.14",
"@angular/platform-browser": "^20.3.14",
"@angular/platform-browser-dynamic": "^20.3.14",
"@angular/platform-server": "^20.3.14",
"@angular/router": "^20.3.14",
"@angular/ssr": "^20.3.12",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.6.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"echarts": "~5.4.0",
"ngx-echarts": "~20.0.2",
"ngx-infinite-scroll": "^20.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.25.8",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.8.0",
"zone.js": "~0.14.4"
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/compiler-cli": "^17.3.1",
"@angular/language-service": "^17.3.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"@angular/compiler-cli": "^20.3.14",
"@angular/language-service": "^20.3.14",
"@types/node": "^24.9.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.39.0",
"browser-sync": "^3.0.3",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"ts-node": "~10.9.1",
"typescript": "~5.4.3"
"typescript": "~5.8.3"
},
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.17.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"cypress": "^15.6.0",
"cypress-fail-on-console-error": "~5.1.1",
"cypress-wait-until": "^3.0.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.1.0"
"start-server-and-test": "~2.1.2"
},
"scarfSettings": {
"enabled": false

View file

@ -1,8 +1,6 @@
const fs = require('fs');
const PROXY_CONFIG = require('./proxy.conf');
const addApiKeyHeader = (proxyReq, req, res) => {
const addApiKeyHeader = (proxyReq) => {
if (process.env.MEMPOOL_CI_API_KEY) {
proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY);
}

View file

@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from './app-routing.module';
@ -50,7 +50,6 @@ const providers = [
FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe,
DatePipe,
AppPreloadingStrategy,
ServicesApiServices,
PreloadService,
@ -58,20 +57,19 @@ const providers = [
{ provide: ZONE_SERVICE, useClass: ZoneService },
];
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
SharedModule,
],
providers: providers,
bootstrap: [AppComponent]
})
@NgModule({ declarations: [
AppComponent,
],
bootstrap: [AppComponent], imports: [BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
SharedModule
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
DatePipe,
...providers
] })
export class AppModule { }
@NgModule({})

View file

@ -5,6 +5,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
selector: 'app-about-sponsors',
templateUrl: './about-sponsors.component.html',
styleUrls: ['./about-sponsors.component.scss'],
standalone: false,
})
export class AboutSponsorsComponent {
@Input() host = 'https://mempool.space';

View file

@ -277,9 +277,9 @@
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
<img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span>
<a href="https://gitlab.com/nirvati-ug/nirvati" target="_blank" title="Nirvati">
<img class="image" src="/resources/profile/nirvati.svg" />
<span>Nirvati</span>
</a>
<a href="https://github.com/fort-nix/nix-bitcoin" target="_blank" title="nix-bitcoin">
<img class="image" src="/resources/profile/nix-bitcoin.png" />
@ -427,16 +427,11 @@
</div>
</ng-container>
<div class="maintainers" id="project-maintainers">
<h3 i18n="about.maintainers">Project Maintainers</h3>
<div class="managers" id="project-managers">
<h3 i18n="about.project-manager">Powered By</h3>
<div class="wrapper">
<a href="https://x.com/softsimon_" target="_blank" title="softsimon">
<img class="image" src="/resources/profile/softsimon.jpg" />
<span>softsimon</span>
</a>
<a href="https://x.com/wiz" target="_blank" title="wiz">
<img class="image" src="/resources/profile/wiz.png" />
<span>wiz</span>
<a href="https://wiz.biz" target="_blank" title="wiz & associates">
<img class="wiz-logo" src="/resources/wiz.png" />
</a>
</div>
</div>
@ -444,7 +439,7 @@
<div class="copyright">
<div class="title">
Copyright &copy; 2019-2025<br>
Mempool Space K.K.<br>
Mempool Holdings S.A. de C.V.<br>
and other shadowy super-coders
</div>
<p>
@ -460,7 +455,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Holdings S.A. de C.V. in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View file

@ -57,7 +57,8 @@
.alliances,
.enterprise-sponsor,
.community-integrations-sponsor,
.maintainers {
.maintainers,
.managers {
margin-top: 30px;
margin-bottom: 68px;
scroll-margin: 30px;
@ -67,7 +68,8 @@
}
}
.maintainers {
.maintainers,
.managers {
margin-bottom: 50px;
}
@ -259,6 +261,12 @@
height: 64px;
}
.wiz-logo {
width: 260px;
max-width: 90vw;
margin: auto;
}
.enterprise-sponsor {
.wrapper {
display: flex;

View file

@ -16,6 +16,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent implements OnInit {

View file

@ -54,7 +54,8 @@ type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' |
@Component({
selector: 'app-accelerate-checkout',
templateUrl: './accelerate-checkout.component.html',
styleUrls: ['./accelerate-checkout.component.scss']
styleUrls: ['./accelerate-checkout.component.scss'],
standalone: false,
})
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() tx: Transaction;

View file

@ -17,6 +17,7 @@ interface GraphBar {
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
standalone: false,
})
export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() tx: Transaction;

View file

@ -4,6 +4,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/cor
selector: 'app-acceleration-timeline-tooltip',
templateUrl: './acceleration-timeline-tooltip.component.html',
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
standalone: false,
})
export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() accelerationInfo: any;

View file

@ -8,6 +8,7 @@ import { MiningService } from '@app/services/mining.service';
selector: 'app-acceleration-timeline',
templateUrl: './acceleration-timeline.component.html',
styleUrls: ['./acceleration-timeline.component.scss'],
standalone: false,
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;

View file

@ -27,6 +27,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDestroy {
@Input() widget: boolean = false;

View file

@ -14,6 +14,7 @@ export type AccelerationStats = {
templateUrl: './acceleration-stats.component.html',
styleUrls: ['./acceleration-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AccelerationStatsComponent implements OnInit, OnChanges {
@Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';

View file

@ -13,6 +13,7 @@ import { MiningService } from '@app/services/mining.service';
templateUrl: './accelerations-list.component.html',
styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AccelerationsListComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;

View file

@ -26,6 +26,7 @@ interface AccelerationBlock extends BlockExtended {
selector: 'app-accelerator-dashboard',
templateUrl: './accelerator-dashboard.component.html',
styleUrls: ['./accelerator-dashboard.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
@ -51,7 +52,6 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
private serviceApiServices: ServicesApiServices,
private audioService: AudioService,
private stateService: StateService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.seoService.setTitle($localize`:@@6b867dc61c6a92f3229f1950f9f2d414790cce95:Accelerator Dashboard`);

View file

@ -21,6 +21,7 @@ function toRGB({r,g,b}): string {
templateUrl: './active-acceleration-box.component.html',
styleUrls: ['./active-acceleration-box.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class ActiveAccelerationBox implements OnChanges {
@Input() acceleratedBy?: number[];

View file

@ -10,6 +10,7 @@ import { WebsocketService } from '@app/services/websocket.service';
templateUrl: './pending-stats.component.html',
styleUrls: ['./pending-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class PendingStatsComponent implements OnInit {
@Input() accelerations$: Observable<Acceleration[]>;

View file

@ -5,6 +5,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inpu
templateUrl: './acceleration-sparkles.component.html',
styleUrls: ['./acceleration-sparkles.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AccelerationSparklesComponent implements OnChanges {
@Input() arrow: ElementRef<HTMLDivElement>;

View file

@ -32,6 +32,7 @@ const periodSeconds = {
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressGraphComponent implements OnChanges, OnDestroy {

View file

@ -14,7 +14,8 @@ import { AddressInformation } from '@interfaces/node-api.interface';
@Component({
selector: 'app-address-group',
templateUrl: './address-group.component.html',
styleUrls: ['./address-group.component.scss']
styleUrls: ['./address-group.component.scss'],
standalone: false,
})
export class AddressGroupComponent implements OnInit, OnDestroy {
network = '';

View file

@ -8,6 +8,7 @@ import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
templateUrl: './address-labels.component.html',
styleUrls: ['./address-labels.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AddressLabelsComponent implements OnChanges {
network = '';

View file

@ -9,6 +9,7 @@ import { PriceService } from '@app/services/price.service';
selector: 'app-address-transactions-widget',
templateUrl: './address-transactions-widget.component.html',
styleUrls: ['./address-transactions-widget.component.scss'],
standalone: false,
})
export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, OnDestroy {
@Input() address: string;

View file

@ -15,7 +15,8 @@ import { AddressInformation } from '@interfaces/node-api.interface';
@Component({
selector: 'app-address-preview',
templateUrl: './address-preview.component.html',
styleUrls: ['./address-preview.component.scss']
styleUrls: ['./address-preview.component.scss'],
standalone: false,
})
export class AddressPreviewComponent implements OnInit, OnDestroy {
network = '';

View file

@ -92,7 +92,8 @@ class AddressStats implements ChainStats {
@Component({
selector: 'app-address',
templateUrl: './address.component.html',
styleUrls: ['./address.component.scss']
styleUrls: ['./address.component.scss'],
standalone: false,
})
export class AddressComponent implements OnInit, OnDestroy {
network = '';
@ -284,8 +285,8 @@ export class AddressComponent implements OnInit, OnDestroy {
}
this.isLoadingTransactions = false;
let addressVin: Vin[] = [];
let vinIds: string[] = [];
const addressVin: Vin[] = [];
const vinIds: string[] = [];
for (const tx of this.transactions) {
tx.vin.forEach((v, index) => {
if (v.prevout?.scriptpubkey_address === this.address.address) {
@ -295,7 +296,7 @@ export class AddressComponent implements OnInit, OnDestroy {
});
}
this.addressTypeInfo.processInputs(addressVin, vinIds);
this.hasTapTree = this.addressTypeInfo.tapscript && this.addressTypeInfo.scripts.values().next().value.scriptPath.length / 2 > 33;
this.hasTapTree = this.addressTypeInfo.tapscript && this.addressTypeInfo.scripts.values().next().value.taprootInfo.scriptPath.merkleBranches.length > 0;
// hack to trigger change detection
this.addressTypeInfo = this.addressTypeInfo.clone();

View file

@ -14,6 +14,7 @@ import { formatNumber } from '@angular/common';
templateUrl: './addresses-treemap.component.html',
styleUrls: ['./addresses-treemap.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AddressesTreemap implements OnChanges {
@Input() addresses: Address[];

View file

@ -7,6 +7,7 @@ import { StateService } from '@app/services/state.service';
selector: 'app-amount-selector',
templateUrl: './amount-selector.component.html',
styleUrls: ['./amount-selector.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AmountSelectorComponent implements OnInit {

View file

@ -8,6 +8,7 @@ import { Price } from '@app/services/price.service';
templateUrl: './amount.component.html',
styleUrls: ['./amount.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AmountComponent implements OnInit, OnDestroy {
conversions$: Observable<any>;

View file

@ -11,6 +11,7 @@ import { SeoService } from '@app/services/seo.service';
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: false,
providers: [NgbTooltipConfig]
})
export class AppComponent implements OnInit {

View file

@ -11,6 +11,7 @@ import { environment } from '@environments/environment';
templateUrl: './asset-circulation.component.html',
styleUrls: ['./asset-circulation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AssetCirculationComponent implements OnInit {
@Input() assetId: string;

View file

@ -16,7 +16,8 @@ import { moveDec } from '@app/bitcoin.utils';
@Component({
selector: 'app-asset',
templateUrl: './asset.component.html',
styleUrls: ['./asset.component.scss']
styleUrls: ['./asset.component.scss'],
standalone: false,
})
export class AssetComponent implements OnInit, OnDestroy {
network = '';

View file

@ -8,7 +8,8 @@ import { AssetsService } from '@app/services/assets.service';
@Component({
selector: 'app-asset-group',
templateUrl: './asset-group.component.html',
styleUrls: ['./asset-group.component.scss']
styleUrls: ['./asset-group.component.scss'],
standalone: false,
})
export class AssetGroupComponent implements OnInit {
group$: Observable<any>;

View file

@ -6,7 +6,8 @@ import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-assets-featured',
templateUrl: './assets-featured.component.html',
styleUrls: ['./assets-featured.component.scss']
styleUrls: ['./assets-featured.component.scss'],
standalone: false,
})
export class AssetsFeaturedComponent implements OnInit {
featuredAssets$: Observable<any>;

View file

@ -14,7 +14,8 @@ import { environment } from '@environments/environment';
@Component({
selector: 'app-assets-nav',
templateUrl: './assets-nav.component.html',
styleUrls: ['./assets-nav.component.scss']
styleUrls: ['./assets-nav.component.scss'],
standalone: false,
})
export class AssetsNavComponent implements OnInit {
@ViewChild('instance', {static: true}) instance: NgbTypeahead;

View file

@ -13,7 +13,8 @@ import { StateService } from '@app/services/state.service';
selector: 'app-assets',
templateUrl: './assets.component.html',
styleUrls: ['./assets.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AssetsComponent implements OnInit {
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;

View file

@ -9,6 +9,7 @@ import { Observable, catchError, of } from 'rxjs';
templateUrl: './balance-widget.component.html',
styleUrls: ['./balance-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class BalanceWidgetComponent implements OnInit, OnChanges {
@Input() address: string;

View file

@ -8,7 +8,8 @@ import { ServicesApiServices } from '@app/services/services-api.service';
@Component({
selector: 'app-bitcoin-invoice',
templateUrl: './bitcoin-invoice.component.html',
styleUrls: ['./bitcoin-invoice.component.scss']
styleUrls: ['./bitcoin-invoice.component.scss'],
standalone: false,
})
export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
@Input() invoice;

View file

@ -26,6 +26,7 @@ import { ActivatedRoute, Router } from '@angular/router';
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockFeeRatesGraphComponent implements OnInit {

View file

@ -26,6 +26,7 @@ import { StateService } from '@app/services/state.service';
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockFeesGraphComponent implements OnInit {

View file

@ -27,6 +27,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockFeesSubsidyGraphComponent implements OnInit {

View file

@ -8,6 +8,7 @@ import { Subscription } from 'rxjs';
selector: 'app-block-filters',
templateUrl: './block-filters.component.html',
styleUrls: ['./block-filters.component.scss'],
standalone: false,
})
export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
@Input() cssWidth: number = 800;

View file

@ -24,6 +24,7 @@ import { StateService } from '@app/services/state.service';
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockHealthGraphComponent implements OnInit {

View file

@ -35,6 +35,7 @@ const unmatchedContrastAuditColors = {
selector: 'app-block-overview-graph',
templateUrl: './block-overview-graph.component.html',
styleUrls: ['./block-overview-graph.component.scss'],
standalone: false,
})
export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() isLoading: boolean;

View file

@ -9,6 +9,7 @@ import { Block } from '@interfaces/electrs.interface.js';
selector: 'app-block-overview-tooltip',
templateUrl: './block-overview-tooltip.component.html',
styleUrls: ['./block-overview-tooltip.component.scss'],
standalone: false,
})
export class BlockOverviewTooltipComponent implements OnChanges {
@Input() tx: TransactionStripped | void;

View file

@ -26,6 +26,7 @@ import { StateService } from '@app/services/state.service';
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockRewardsGraphComponent implements OnInit {

View file

@ -24,6 +24,7 @@ import { StateService } from '@app/services/state.service';
z-index: 99;
}
`],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockSizesWeightsGraphComponent implements OnInit {

View file

@ -28,7 +28,8 @@ function bestFitResolution(min, max, n): number {
@Component({
selector: 'app-block-view',
templateUrl: './block-view.component.html',
styleUrls: ['./block-view.component.scss']
styleUrls: ['./block-view.component.scss'],
standalone: false,
})
export class BlockViewComponent implements OnInit, OnDestroy {
network = '';

View file

@ -15,7 +15,8 @@ import { ServicesApiServices } from '@app/services/services-api.service';
@Component({
selector: 'app-block-preview',
templateUrl: './block-preview.component.html',
styleUrls: ['./block-preview.component.scss']
styleUrls: ['./block-preview.component.scss'],
standalone: false,
})
export class BlockPreviewComponent implements OnInit, OnDestroy {
network = '';
@ -138,7 +139,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
return of(transactions);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500
this.stateService.env.ACCELERATOR === true && block.height > 819500 && this.stateService.network === ''
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(
catchError(() => {

View file

@ -10,6 +10,7 @@ import { PreloadService } from '@app/services/preload.service';
selector: 'app-block-transactions',
templateUrl: './block-transactions.component.html',
styleUrl: './block-transactions.component.scss',
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockTransactionsComponent implements OnInit {

View file

@ -32,6 +32,7 @@ interface ComparisonStats {
@Component({
selector: 'app-block',
templateUrl: './block.component.html',
standalone: false,
styleUrls: ['./block.component.scss'],
styles: [`
.loadingGraphs {
@ -384,7 +385,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.accelerationsSubscription = this.block$.pipe(
switchMap((block) => {
return this.stateService.env.ACCELERATOR === true && block.height > 819500
return this.stateService.env.ACCELERATOR === true && block.height > 819500 && this.stateService.network === ''
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);

View file

@ -15,6 +15,7 @@ interface BlockchainBlock extends BlockExtended {
selector: 'app-blockchain-blocks',
templateUrl: './blockchain-blocks.component.html',
styleUrls: ['./blockchain-blocks.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {

View file

@ -7,6 +7,7 @@ import { StorageService } from '@app/services/storage.service';
selector: 'app-blockchain',
templateUrl: './blockchain.component.html',
styleUrls: ['./blockchain.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {

View file

@ -15,6 +15,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
selector: 'app-blocks-list',
templateUrl: './blocks-list.component.html',
styleUrls: ['./blocks-list.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlocksList implements OnInit {

View file

@ -9,6 +9,7 @@ import { WebsocketService } from '@app/services/websocket.service';
selector: 'app-calculator',
templateUrl: './calculator.component.html',
styleUrls: ['./calculator.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CalculatorComponent implements OnInit {

View file

@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/c
selector: 'app-change',
templateUrl: './change.component.html',
styleUrls: ['./change.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChangeComponent implements OnChanges {

View file

@ -4,6 +4,7 @@ import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@a
selector: 'app-clipboard',
templateUrl: './clipboard.component.html',
styleUrls: ['./clipboard.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClipboardComponent {

View file

@ -6,6 +6,7 @@ import { StateService } from '@app/services/state.service';
selector: 'app-clock-face',
templateUrl: './clock-face.component.html',
styleUrls: ['./clock-face.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {

View file

@ -11,6 +11,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
selector: 'app-clock',
templateUrl: './clock.component.html',
styleUrls: ['./clock.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockComponent implements OnInit {

View file

@ -6,6 +6,7 @@ import { StateService } from '@app/services/state.service';
selector: 'app-clockchain',
templateUrl: './clockchain.component.html',
styleUrls: ['./clockchain.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {

View file

@ -33,6 +33,7 @@ interface MempoolStatsData {
selector: 'app-custom-dashboard',
templateUrl: './custom-dashboard.component.html',
styleUrls: ['./custom-dashboard.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
@ -89,7 +90,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
private websocketService: WebsocketService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.widgets = this.stateService.env.customize?.dashboard.widgets || [];

View file

@ -18,6 +18,7 @@ import { StateService } from '@app/services/state.service';
z-index: 99;
}
`],
standalone: false,
})
export class DifficultyAdjustmentsTable implements OnInit {
hashrateObservable$: Observable<any>;

View file

@ -23,6 +23,7 @@ interface EpochProgress {
selector: 'app-difficulty-mining',
templateUrl: './difficulty-mining.component.html',
styleUrls: ['./difficulty-mining.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyMiningComponent implements OnInit {

View file

@ -22,6 +22,7 @@ const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
selector: 'app-difficulty-tooltip',
templateUrl: './difficulty-tooltip.component.html',
styleUrls: ['./difficulty-tooltip.component.scss'],
standalone: false,
})
export class DifficultyTooltipComponent implements OnChanges {
@Input() status: string | void;

View file

@ -39,6 +39,7 @@ const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
selector: 'app-difficulty',
templateUrl: './difficulty.component.html',
styleUrls: ['./difficulty.component.scss'],
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyComponent implements OnInit {

View file

@ -45,6 +45,7 @@ interface BlockInfo extends BlockExtended {
])
]),
],
standalone: false,
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';

View file

@ -11,7 +11,8 @@ import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-faucet',
templateUrl: './faucet.component.html',
styleUrls: ['./faucet.component.scss']
styleUrls: ['./faucet.component.scss'],
standalone: false,
})
export class FaucetComponent implements OnInit, OnDestroy {
loading = true;

View file

@ -11,6 +11,7 @@ import { Subscription } from 'rxjs';
templateUrl: './fee-distribution-graph.component.html',
styleUrls: ['./fee-distribution-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestroy {
@Input() feeRange: number[];

Some files were not shown because too many files have changed in this diff Show more