mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge branch 'master' into mononaut/fractional-fees
This commit is contained in:
commit
0ecce2801e
299 changed files with 19628 additions and 19685 deletions
145
.github/workflows/backend-integration.yml
vendored
Normal file
145
.github/workflows/backend-integration.yml
vendored
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
20
backend/docker-compose.test.yml
Normal file
20
backend/docker-compose.test.yml
Normal 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
|
||||
|
||||
|
|
@ -16,5 +16,9 @@ const config: Config.InitialOptions = {
|
|||
setupFiles: [
|
||||
"./testSetup.ts",
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
"/node_modules/",
|
||||
"/__integration_tests__/",
|
||||
],
|
||||
}
|
||||
export default config;
|
||||
|
|
|
|||
21
backend/jest.integration.config.ts
Normal file
21
backend/jest.integration.config.ts
Normal 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;
|
||||
|
||||
72
backend/jest.integration.setup.ts
Normal file
72
backend/jest.integration.setup.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
102
backend/jest.integration.teardown.ts
Normal file
102
backend/jest.integration.teardown.ts
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
163
backend/mempool-config.test.json
Normal file
163
backend/mempool-config.test.json
Normal 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
8593
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
backend/scripts/debug-integration-tests.sh
Executable file
40
backend/scripts/debug-integration-tests.sh
Executable 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
111
backend/scripts/test-with-db.sh
Executable 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}"
|
||||
|
||||
134
backend/src/__integration_tests__/blocks-repository.test.ts
Normal file
134
backend/src/__integration_tests__/blocks-repository.test.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
97
backend/src/__integration_tests__/database-migration.test.ts
Normal file
97
backend/src/__integration_tests__/database-migration.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
122
backend/src/__integration_tests__/pools-repository.test.ts
Normal file
122
backend/src/__integration_tests__/pools-repository.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
190
backend/src/__integration_tests__/test-helpers.ts
Normal file
190
backend/src/__integration_tests__/test-helpers.ts
Normal 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
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
14
backend/testSetup.integration.ts
Normal file
14
backend/testSetup.integration.ts
Normal 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)
|
||||
|
||||
3
contributors/AaronDewes.txt
Normal file
3
contributors/AaronDewes.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of October 31, 2025.
|
||||
|
||||
Signed: AaronDewes
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"component": "twitter",
|
||||
"mobileOrder": 5,
|
||||
"props": {
|
||||
"handle": "Metaplanet_JP"
|
||||
"handle": "Metaplanet"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
92
frontend/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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
27610
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({})
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 © 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®, Mempool Accelerator®, Mempool Enterprise®, Mempool Wallet™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, 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®, Mempool Accelerator®, Mempool Enterprise®, Mempool Wallet™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, 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 <https://mempool.space/trademark-policy>.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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[]>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const periodSeconds = {
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { StateService } from '@app/services/state.service';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockFeesGraphComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { StateService } from '@app/services/state.service';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockHealthGraphComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { StateService } from '@app/services/state.service';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockRewardsGraphComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { StateService } from '@app/services/state.service';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { StateService } from '@app/services/state.service';
|
|||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
standalone: false,
|
||||
})
|
||||
export class DifficultyAdjustmentsTable implements OnInit {
|
||||
hashrateObservable$: Observable<any>;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ interface BlockInfo extends BlockExtended {
|
|||
])
|
||||
]),
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class EightBlocksComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue