Merge branch 'master' into natsoni/fix-network-errors

This commit is contained in:
softsimon 2024-04-02 14:31:32 +09:00 committed by GitHub
commit 974eaeb02f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 5941 additions and 6488 deletions

View File

@ -35,7 +35,7 @@ jobs:
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1 # Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023 # Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
with: with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }} toolchain: ${{ steps.gettoolchain.outputs.toolchain }}

View File

@ -7,13 +7,14 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
class Audit { class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { : { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator const accelerated: string[] = []; // prioritized by the mempool accelerator
@ -68,20 +69,27 @@ class Audit {
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
// these displaced transactions should occupy the first N weight units of the next projected block // these displaced transactions should occupy the first N weight units of the next projected block
let displacedWeightRemaining = displacedWeight; let displacedWeightRemaining = displacedWeight + 4000;
let index = 0; let index = 0;
let lastFeeRate = Infinity; let lastFeeRate = Infinity;
let failures = 0; let failures = 0;
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) { let blockIndex = 1;
const txid = projectedBlocks[1].transactionIds[index]; while (projectedBlocks[blockIndex] && failures < 500) {
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
index = 0;
blockIndex++;
}
const txid = projectedBlocks[blockIndex].transactionIds[index];
const tx = mempool[txid]; const tx = mempool[txid];
if (tx) { if (tx) {
const fits = (tx.weight - displacedWeightRemaining) < 4000; const fits = (tx.weight - displacedWeightRemaining) < 4000;
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate; // 0.005 margin of error for any remaining vsize rounding issues
const feeMatches = tx.effectiveFeePerVsize >= (lastFeeRate - 0.005);
if (fits || feeMatches) { if (fits || feeMatches) {
isDisplaced[txid] = true; isDisplaced[txid] = true;
if (fits) { if (fits) {
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize); // (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize) attempts to correct for vsize rounding in the simple non-CPFP case
lastFeeRate = Math.min(lastFeeRate, (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize));
} }
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) { if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= tx.weight; displacedWeightRemaining -= tx.weight;
@ -106,7 +114,11 @@ class Audit {
if (rbfCache.has(tx.txid)) { if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid); rbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) { } else if (!isDisplaced[tx.txid]) {
added.push(tx.txid); if (mempool[tx.txid]) {
prioritized.push(tx.txid);
} else {
added.push(tx.txid);
}
} }
overflowWeight += tx.weight; overflowWeight += tx.weight;
} }
@ -155,6 +167,7 @@ class Audit {
return { return {
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
prioritized,
fresh, fresh,
sigop: [], sigop: [],
fullrbf: rbf, fullrbf: rbf,

View File

@ -552,6 +552,7 @@ export class Common {
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined, acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize, rate: tx.effectiveFeePerVsize,
time: tx.firstSeen || undefined,
}; };
} }

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 75; private static currentVersion = 76;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -654,6 +654,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"'); await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
await this.updateToSchemaVersion(75); await this.updateToSchemaVersion(75);
} }
if (databaseSchemaVersion < 76 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(76);
}
} }
/** /**

View File

@ -598,7 +598,8 @@ class MempoolBlocks {
tx.value, tx.value,
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100, Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
tx.flags, tx.flags,
1 tx.time || 0,
1,
]; ];
} else { } else {
return [ return [
@ -608,6 +609,7 @@ class MempoolBlocks {
tx.value, tx.value,
Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100, Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
tx.flags, tx.flags,
tx.time || 0,
]; ];
} }
} }

View File

@ -83,6 +83,7 @@ class WebsocketHandler {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
this.updateSocketDataFields({ this.updateSocketDataFields({
'backend': config.MEMPOOL.BACKEND,
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(), 'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks, 'blocks': _blocks,
@ -868,7 +869,7 @@ class WebsocketHandler {
} }
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@ -894,6 +895,7 @@ class WebsocketHandler {
height: block.height, height: block.height,
hash: block.id, hash: block.id,
addedTxs: added, addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored, missingTxs: censored,
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop, sigopTxs: sigop,

View File

@ -37,6 +37,7 @@ export interface BlockAudit {
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[], fullrbfTxs: string[],
addedTxs: string[], addedTxs: string[],
prioritizedTxs: string[],
acceleratedTxs: string[], acceleratedTxs: string[],
matchRate: number, matchRate: number,
expectedFees?: number, expectedFees?: number,
@ -200,6 +201,7 @@ export interface TransactionStripped {
value: number; value: number;
acc?: boolean; acc?: boolean;
rate?: number; // effective fee rate rate?: number; // effective fee rate
time?: number;
} }
export interface TransactionClassified extends TransactionStripped { export interface TransactionClassified extends TransactionStripped {
@ -207,7 +209,7 @@ export interface TransactionClassified extends TransactionStripped {
} }
// [txid, fee, vsize, value, rate, flags, acceleration?] // [txid, fee, vsize, value, rate, flags, acceleration?]
export type TransactionCompressed = [string, number, number, number, number, number, 1?]; export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
// [txid, rate, flags, acceleration?] // [txid, rate, flags, acceleration?]
export type MempoolDeltaChange = [string, number, number, (1|0)]; export type MempoolDeltaChange = [string, number, number, (1|0)];

View File

@ -114,6 +114,7 @@ class AuditReplication {
time: auditSummary.timestamp || auditSummary.time, time: auditSummary.timestamp || auditSummary.time,
missingTxs: auditSummary.missingTxs || [], missingTxs: auditSummary.missingTxs || [],
addedTxs: auditSummary.addedTxs || [], addedTxs: auditSummary.addedTxs || [],
prioritizedTxs: auditSummary.prioritizedTxs || [],
freshTxs: auditSummary.freshTxs || [], freshTxs: auditSummary.freshTxs || [],
sigopTxs: auditSummary.sigopTxs || [], sigopTxs: auditSummary.sigopTxs || [],
fullrbfTxs: auditSummary.fullrbfTxs || [], fullrbfTxs: auditSummary.fullrbfTxs || [],

View File

@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight) await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@ -66,6 +66,7 @@ class BlocksAuditRepositories {
template, template,
missing_txs as missingTxs, missing_txs as missingTxs,
added_txs as addedTxs, added_txs as addedTxs,
prioritized_txs as prioritizedTxs,
fresh_txs as freshTxs, fresh_txs as freshTxs,
sigop_txs as sigopTxs, sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs, fullrbf_txs as fullrbfTxs,
@ -81,6 +82,7 @@ class BlocksAuditRepositories {
if (rows.length) { if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs); rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);

View File

@ -1,4 +1,4 @@
FROM node:20.11.1-buster-slim AS builder FROM node:20.12.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV MEMPOOL_COMMIT_HASH=${commitHash} ENV MEMPOOL_COMMIT_HASH=${commitHash}
@ -17,7 +17,7 @@ ENV PATH="/root/.cargo/bin:$PATH"
RUN npm install --omit=dev --omit=optional RUN npm install --omit=dev --omit=optional
RUN npm run package RUN npm run package
FROM node:20.11.1-buster-slim FROM node:20.12.0-buster-slim
WORKDIR /backend WORKDIR /backend

View File

@ -1,4 +1,4 @@
FROM node:20.11.1-buster-slim AS builder FROM node:20.12.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash} ENV DOCKER_COMMIT_HASH=${commitHash}

View File

@ -19,6 +19,7 @@
"@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1, "@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1, "@typescript-eslint/explicit-function-return-type": 1,
"@typescript-eslint/no-unused-vars": 1,
"no-case-declarations": 1, "no-case-declarations": 1,
"no-console": 1, "no-console": 1,
"no-constant-condition": 1, "no-constant-condition": 1,

View File

@ -223,11 +223,11 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "mempool:build" "buildTarget": "mempool:build"
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "mempool:build:production" "buildTarget": "mempool:build:production"
}, },
"local": { "local": {
"proxyConfig": "proxy.conf.local.js", "proxyConfig": "proxy.conf.local.js",
@ -264,7 +264,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "mempool:build" "buildTarget": "mempool:build"
} }
}, },
"e2e": { "e2e": {
@ -303,7 +303,7 @@
} }
}, },
"serve-ssr": { "serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server", "builder": "@angular-devkit/build-angular:ssr-dev-server",
"options": { "options": {
"browserTarget": "mempool:build", "browserTarget": "mempool:build",
"serverTarget": "mempool:server" "serverTarget": "mempool:server"
@ -318,7 +318,7 @@
} }
}, },
"prerender": { "prerender": {
"builder": "@nguniversal/builders:prerender", "builder": "@angular-devkit/build-angular:prerender",
"options": { "options": {
"browserTarget": "mempool:build:production", "browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production", "serverTarget": "mempool:server:production",

11999
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -64,24 +64,25 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
}, },
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^16.1.1", "@angular-devkit/build-angular": "^17.3.1",
"@angular/animations": "^16.1.1", "@angular/animations": "^17.3.1",
"@angular/cli": "^16.1.1", "@angular/cli": "^17.3.1",
"@angular/common": "^16.1.1", "@angular/common": "^17.3.1",
"@angular/compiler": "^16.1.1", "@angular/compiler": "^17.3.1",
"@angular/core": "^16.1.1", "@angular/core": "^17.3.1",
"@angular/forms": "^16.1.1", "@angular/forms": "^17.3.1",
"@angular/localize": "^16.1.1", "@angular/localize": "^17.3.1",
"@angular/platform-browser": "^16.1.1", "@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^16.1.1", "@angular/platform-browser-dynamic": "^17.3.1",
"@angular/platform-server": "^16.1.1", "@angular/platform-server": "^17.3.1",
"@angular/router": "^16.1.1", "@angular/router": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.13.0", "@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1", "@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1", "@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1", "@fortawesome/free-solid-svg-icons": "~6.5.1",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.2", "bootstrap": "~4.6.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
@ -89,29 +90,29 @@
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.5.0", "echarts": "~5.5.0",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.2.0", "ngx-echarts": "~17.1.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tinyify": "^3.1.0", "esbuild": "^0.20.2",
"tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.6.0",
"zone.js": "~0.13.1" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "^16.1.1", "@angular/compiler-cli": "^17.3.1",
"@angular/language-service": "^16.1.1", "@angular/language-service": "^17.3.1",
"@nguniversal/builders": "16.1.1",
"@nguniversal/express-engine": "16.1.1",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.31.0", "eslint": "^8.57.0",
"browser-sync": "^3.0.0",
"http-proxy-middleware": "~2.0.6", "http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "~4.9.3" "typescript": "~5.4.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",

View File

@ -1,4 +1,3 @@
import 'zone.js/dist/zone-node';
import './src/resources/config.js'; import './src/resources/config.js';
import * as domino from 'domino'; import * as domino from 'domino';

View File

@ -1,12 +1,11 @@
import 'zone.js/dist/zone-node'; import 'zone.js';
import './src/resources/config.js'; import './src/resources/config.js';
import { ngExpressEngine } from '@nguniversal/express-engine'; import { CommonEngine } from '@angular/ssr';
import * as express from 'express'; import * as express from 'express';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as domino from 'domino'; import * as domino from 'domino';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { join } from 'path'; import { join } from 'path';
import { AppServerModule } from './src/main.server'; import { AppServerModule } from './src/main.server';
@ -15,6 +14,8 @@ import { existsSync } from 'fs';
import { ResizeObserver } from './shims'; import { ResizeObserver } from './shims';
const commonEngine = new CommonEngine();
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString(); const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template); const win = domino.createWindow(template);
@ -58,35 +59,32 @@ global['localStorage'] = {
export function app(locale: string): express.Express { export function app(locale: string): express.Express {
const server = express(); const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`); const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; const indexHtml = join(distFolder, 'index.html');
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html'); server.set('view engine', 'html');
server.set('views', distFolder); server.set('views', distFolder);
// static file handler so we send HTTP 404 to nginx // static file handler so we send HTTP 404 to nginx
server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false })); server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false }));
// handle page routes // handle page routes
server.get('/**', getLocalizedSSR(indexHtml)); server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server; return server;
} }
function getLocalizedSSR(indexHtml) {
return (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
}
}
// only used for development mode // only used for development mode
function run(): void { function run(): void {
@ -107,6 +105,4 @@ const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || ''; const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run(); run();
} }
export * from './src/main.server';

View File

@ -51,7 +51,7 @@
</div> </div>
</div> </div>
<ng-container *ngIf="address && transactions && transactions.length > 2"> <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br> <br>
<div class="box"> <div class="box">
<div class="row"> <div class="row">

View File

@ -44,7 +44,7 @@ export class AddressComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private stateService: StateService, public stateService: StateService,
private audioService: AudioService, private audioService: AudioService,
private apiService: ApiService, private apiService: ApiService,
private seoService: SeoService, private seoService: SeoService,

View File

@ -14,6 +14,7 @@
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
[filterFlags]="activeFilterFlags" [filterFlags]="activeFilterFlags"
[filterMode]="filterMode" [filterMode]="filterMode"
[relativeTime]="relativeTime"
></app-block-overview-tooltip> ></app-block-overview-tooltip>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters> <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder"> <div *ngIf="!webGlEnabled" class="placeholder">

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/node-api.interface';
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene'; import BlockScene from './block-scene';
import TxSprite from './tx-sprite'; import TxSprite from './tx-sprite';
@ -20,7 +20,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity), added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity), prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
}; };
@ -46,6 +46,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null; @Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and'; @Input() filterMode: FilterMode = 'and';
@Input() relativeTime: number | null;
@Input() blockConversion: Price; @Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();

View File

@ -1,6 +1,6 @@
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view'; import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
import { defaultColorFunction } from './utils'; import { defaultColorFunction } from './utils';

View File

@ -32,7 +32,8 @@ export default class TxView implements TransactionStripped {
rate?: number; rate?: number;
flags: number; flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@ -53,6 +54,7 @@ export default class TxView implements TransactionStripped {
this.scene = scene; this.scene = scene;
this.context = tx.context; this.context = tx.context;
this.txid = tx.txid; this.txid = tx.txid;
this.time = tx.time || 0;
this.fee = tx.fee; this.fee = tx.fee;
this.vsize = tx.vsize; this.vsize = tx.vsize;
this.value = tx.value; this.value = tx.value;

View File

@ -45,7 +45,7 @@ export const defaultAuditColors = {
censored: hexToColor('f344df'), censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'), added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'), accelerated: hexToColor('8F5FF6'),
}; };
@ -81,6 +81,8 @@ export function defaultColorFunction(
return auditColors.missing; return auditColors.missing;
case 'added': case 'added':
return auditColors.added; return auditColors.added;
case 'prioritized':
return auditColors.prioritized;
case 'selected': case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated': case 'accelerated':

View File

@ -14,6 +14,26 @@
<a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a> <a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a>
</td> </td>
</tr> </tr>
<tr *ngIf="time">
<ng-container [ngSwitch]="timeMode">
<ng-container *ngSwitchCase="'mempool'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="since" [time]="time" [fastRender]="true"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'missed'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="before" [time]="relativeTime - time"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'after'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="span" [time]="time - relativeTime"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'mined'">
<td class="label" i18n="transaction.confirmed-after|Transaction confirmed after">Confirmed</td>
<td class="value"><i><app-time kind="span" [time]="relativeTime - time"></app-time></i></td>
</ng-container>
</ng-container>
</tr>
<tr> <tr>
<td class="label" i18n="dashboard.latest-transactions.amount">Amount</td> <td class="label" i18n="dashboard.latest-transactions.amount">Amount</td>
<td class="value"><app-amount [blockConversion]="blockConversion" [satoshis]="value" [noFiat]="true"></app-amount></td> <td class="value"><app-amount [blockConversion]="blockConversion" [satoshis]="value" [noFiat]="true"></app-amount></td>
@ -54,6 +74,7 @@
<span *ngSwitchCase="'fresh'" class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span> <span *ngSwitchCase="'fresh'" class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span>
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span> <span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="transaction.audit.added">Added</span> <span *ngSwitchCase="'added'" class="badge badge-warning" i18n="transaction.audit.added">Added</span>
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="transaction.audit.prioritized">Prioritized</span>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span> <span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span> <span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span> <span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@ -3,6 +3,7 @@ import { Position } from '../../components/block-overview-graph/sprite-types.js'
import { Price } from '../../services/price.service'; import { Price } from '../../services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js'; import { TransactionStripped } from '../../interfaces/node-api.interface.js';
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils'; import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils';
import { Block } from '../../interfaces/electrs.interface.js';
@Component({ @Component({
selector: 'app-block-overview-tooltip', selector: 'app-block-overview-tooltip',
@ -11,6 +12,7 @@ import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/fi
}) })
export class BlockOverviewTooltipComponent implements OnChanges { export class BlockOverviewTooltipComponent implements OnChanges {
@Input() tx: TransactionStripped | void; @Input() tx: TransactionStripped | void;
@Input() relativeTime?: number;
@Input() cursorPosition: Position; @Input() cursorPosition: Position;
@Input() clickable: boolean; @Input() clickable: boolean;
@Input() auditEnabled: boolean = false; @Input() auditEnabled: boolean = false;
@ -19,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
@Input() filterMode: FilterMode = 'and'; @Input() filterMode: FilterMode = 'and';
txid = ''; txid = '';
time: number = 0;
fee = 0; fee = 0;
value = 0; value = 0;
vsize = 1; vsize = 1;
@ -26,6 +29,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
effectiveRate; effectiveRate;
acceleration; acceleration;
hasEffectiveRate: boolean = false; hasEffectiveRate: boolean = false;
timeMode: 'mempool' | 'mined' | 'missed' | 'after' = 'mempool';
filters: Filter[] = []; filters: Filter[] = [];
activeFilters: { [key: string]: boolean } = {}; activeFilters: { [key: string]: boolean } = {};
@ -56,6 +60,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
if (this.tx && (changes.tx || changes.filterFlags || changes.filterMode)) { if (this.tx && (changes.tx || changes.filterFlags || changes.filterMode)) {
this.txid = this.tx.txid || ''; this.txid = this.tx.txid || '';
this.time = this.tx.time || 0;
this.fee = this.tx.fee || 0; this.fee = this.tx.fee || 0;
this.value = this.tx.value || 0; this.value = this.tx.value || 0;
this.vsize = this.tx.vsize || 1; this.vsize = this.tx.vsize || 1;
@ -72,6 +77,22 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.activeFilters[filter.key] = true; this.activeFilters[filter.key] = true;
} }
} }
if (!this.relativeTime) {
this.timeMode = 'mempool';
} else {
if (this.tx?.context === 'actual' || this.tx?.status === 'found') {
this.timeMode = 'mined';
} else {
const time = this.relativeTime || Date.now();
if (this.time <= time) {
this.timeMode = 'missed';
} else {
this.timeMode = 'after';
}
}
}
this.cd.markForCheck(); this.cd.markForCheck();
} }
} }

View File

@ -8,6 +8,7 @@
[orientation]="'top'" [orientation]="'top'"
[flip]="false" [flip]="false"
[disableSpinner]="true" [disableSpinner]="true"
[relativeTime]="block?.timestamp"
(txClickEvent)="onTxClick($event)" (txClickEvent)="onTxClick($event)"
></app-block-overview-graph> ></app-block-overview-graph>
</div> </div>

View File

@ -117,6 +117,7 @@
[blockConversion]="blockConversion" [blockConversion]="blockConversion"
[showFilters]="true" [showFilters]="true"
[excludeFilters]="['replacement']" [excludeFilters]="['replacement']"
[relativeTime]="block?.timestamp"
(txClickEvent)="onTxClick($event)" (txClickEvent)="onTxClick($event)"
></app-block-overview-graph> ></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
@ -232,7 +233,7 @@
<app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86" <app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit" (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph> [showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div> </div>
<ng-container *ngIf="network !== 'liquid'"> <ng-container *ngIf="network !== 'liquid'">
@ -247,7 +248,7 @@
<app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86" <app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit" (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph> [showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div> </div>
<ng-container *ngIf="network !== 'liquid'"> <ng-container *ngIf="network !== 'liquid'">

View File

@ -371,6 +371,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const inTemplate = {}; const inTemplate = {};
const inBlock = {}; const inBlock = {};
const isAdded = {}; const isAdded = {};
const isPrioritized = {};
const isCensored = {}; const isCensored = {};
const isMissing = {}; const isMissing = {};
const isSelected = {}; const isSelected = {};
@ -394,6 +395,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) { for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true; isAdded[txid] = true;
} }
for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true;
}
for (const txid of blockAudit.missingTxs) { for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true; isCensored[txid] = true;
} }
@ -443,6 +447,8 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = null; tx.status = null;
} else if (isAdded[tx.txid]) { } else if (isAdded[tx.txid]) {
tx.status = 'added'; tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
} else if (inTemplate[tx.txid]) { } else if (inTemplate[tx.txid]) {
tx.status = 'found'; tx.status = 'found';
} else if (isRbf[tx.txid]) { } else if (isRbf[tx.txid]) {

View File

@ -12,6 +12,7 @@
[animationDuration]="animationDuration" [animationDuration]="animationDuration"
[animationOffset]="animationOffset" [animationOffset]="animationOffset"
[disableSpinner]="true" [disableSpinner]="true"
[relativeTime]="blockInfo[i]?.timestamp"
(txClickEvent)="onTxClick($event)" (txClickEvent)="onTxClick($event)"
></app-block-overview-graph> ></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange> <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>

View File

@ -1,6 +1,6 @@
import { HostListener, OnChanges, OnDestroy } from '@angular/core'; import { HostListener, OnChanges, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
import { selectPowerOfTen } from '../../bitcoin.utils'; import { selectPowerOfTen } from '../../bitcoin.utils';

View File

@ -265,8 +265,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
type: 'value', type: 'value',
axisLabel: { axisLabel: {
fontSize: 11, fontSize: 11,
formatter: (value) => { formatter: (value): string => {
return this.weightMode ? value * 4 : value; return this.weightMode ? (value * 4).toString() : value.toString();
} }
}, },
splitLine: { splitLine: {

View File

@ -1,7 +1,8 @@
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface'; import { MempoolBlockDelta } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs'; import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators'; import { switchMap, filter, concatMap, map } from 'rxjs/operators';

View File

@ -3,7 +3,8 @@ import { detectWebGL } from '../../shared/graphs.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, map, tap, filter } from 'rxjs/operators'; import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface'; import { MempoolBlock } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '../../shared/common.utils';

View File

@ -20,10 +20,12 @@
- -
<app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate> <app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate>
</div> </div>
<div *ngIf="showMiningInfo" class="block-size"> <div *ngIf="showMiningInfo$ | async; else noMiningInfo" class="block-size">
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount> <app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div> </div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div> <ng-template #noMiningInfo>
<div class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
</ng-template>
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count"> <div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container> <ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>

View File

@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs'; import { Subscription, Observable, of, combineLatest, BehaviorSubject } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface'; import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -42,6 +42,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
mempoolBlocks$: Observable<MempoolBlock[]>; mempoolBlocks$: Observable<MempoolBlock[]>;
difficultyAdjustments$: Observable<DifficultyAdjustment>; difficultyAdjustments$: Observable<DifficultyAdjustment>;
loadingBlocks$: Observable<boolean>; loadingBlocks$: Observable<boolean>;
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
blocksSubscription: Subscription; blocksSubscription: Subscription;
mempoolBlocksFull: MempoolBlock[] = []; mempoolBlocksFull: MempoolBlock[] = [];
@ -57,10 +58,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
network = ''; network = '';
now = new Date().getTime(); now = new Date().getTime();
timeOffset = 0; timeOffset = 0;
showMiningInfo = false;
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
showMiningInfoSubscription: Subscription;
animateEntry: boolean = false; animateEntry: boolean = false;
blockOffset: number = 155; blockOffset: number = 155;
@ -98,10 +97,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.widthChange.emit(this.mempoolWidth); this.widthChange.emit(this.mempoolWidth);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) { if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.showMiningInfoSubscription = this.stateService.showMiningInfo$.subscribe((showMiningInfo) => { this.showMiningInfo$ = this.stateService.showMiningInfo$;
this.showMiningInfo = showMiningInfo;
this.cd.markForCheck();
});
} }
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
@ -267,7 +263,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.chainTipSubscription.unsubscribe(); this.chainTipSubscription.unsubscribe();
this.keySubscription.unsubscribe(); this.keySubscription.unsubscribe();
this.isTabHiddenSubscription.unsubscribe(); this.isTabHiddenSubscription.unsubscribe();
this.showMiningInfoSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout); clearTimeout(this.resetTransitionTimeout);
} }

View File

@ -411,7 +411,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
padding: [20, 0, 0, 0], padding: [20, 0, 0, 0],
}, },
type: 'time', type: 'time',
boundaryGap: false,
axisLine: { onZero: true }, axisLine: { onZero: true },
axisLabel: { axisLabel: {
margin: 20, margin: 20,

View File

@ -6,7 +6,7 @@
<span class="menu-click text-nowrap ellipsis"> <span class="menu-click text-nowrap ellipsis">
<strong> <strong>
<span *ngIf="user.username.includes('@'); else usernamenospace">{{ user.username }}</span> <span *ngIf="user.username.includes('@'); else usernamenospace">{{ user.username }}</span>
<ng-template #usernamenospace>@{{ user.username }}</ng-template> <ng-template #usernamenospace>&#64;{{ user.username }}</ng-template>
</strong> </strong>
</span> </span>
<span class="badge mr-1 badge-og" *ngIf="user.ogRank"> <span class="badge mr-1 badge-og" *ngIf="user.ogRank">

View File

@ -23,7 +23,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() time: number; @Input() time: number;
@Input() dateString: number; @Input() dateString: number;
@Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain'; @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' = 'plain';
@Input() fastRender = false; @Input() fastRender = false;
@Input() fixedRender = false; @Input() fixedRender = false;
@Input() relative = false; @Input() relative = false;
@ -86,7 +86,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
seconds = Math.floor(this.time); seconds = Math.floor(this.time);
} }
if (seconds < 60) { if (seconds < 1 && this.kind === 'span') {
return $localize`:@@date-base.immediately:Immediately`;
} else if (seconds < 60) {
if (this.relative || this.kind === 'since') { if (this.relative || this.kind === 'since') {
return $localize`:@@date-base.just-now:Just now`; return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until') { } else if (this.kind === 'until') {
@ -206,6 +208,29 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
break; break;
case 'before':
if (number === 1) {
switch (unit) { // singular (1 day)
case 'year': return $localize`:@@time-span:${dateStrings.i18nYear}:DATE: before`; break;
case 'month': return $localize`:@@time-span:${dateStrings.i18nMonth}:DATE: before`; break;
case 'week': return $localize`:@@time-span:${dateStrings.i18nWeek}:DATE: before`; break;
case 'day': return $localize`:@@time-span:${dateStrings.i18nDay}:DATE: before`; break;
case 'hour': return $localize`:@@time-span:${dateStrings.i18nHour}:DATE: before`; break;
case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinute}:DATE: before`; break;
case 'second': return $localize`:@@time-span:${dateStrings.i18nSecond}:DATE: before`; break;
}
} else {
switch (unit) { // plural (2 days)
case 'year': return $localize`:@@time-span:${dateStrings.i18nYears}:DATE: before`; break;
case 'month': return $localize`:@@time-span:${dateStrings.i18nMonths}:DATE: before`; break;
case 'week': return $localize`:@@time-span:${dateStrings.i18nWeeks}:DATE: before`; break;
case 'day': return $localize`:@@time-span:${dateStrings.i18nDays}:DATE: before`; break;
case 'hour': return $localize`:@@time-span:${dateStrings.i18nHours}:DATE: before`; break;
case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinutes}:DATE: before`; break;
case 'second': return $localize`:@@time-span:${dateStrings.i18nSeconds}:DATE: before`; break;
}
}
break;
default: default:
if (number === 1) { if (number === 1) {
switch (unit) { // singular (1 day) switch (unit) { // singular (1 day)

View File

@ -326,7 +326,7 @@
<br> <br>
<p>If you have any questions about this Policy, would like to speak with us about the use of our Marks in ways not described in the Policy, or see any abuse of our Marks, please email us at &lt;legal@mempool.space&gt;</p> <p>If you have any questions about this Policy, would like to speak with us about the use of our Marks in ways not described in the Policy, or see any abuse of our Marks, please email us at &lt;legal&#64;mempool.space&gt;</p>
</ol> </ol>

View File

@ -77,8 +77,9 @@
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span> <span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span>
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template> <ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template> <ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
<ng-template #notSeen><span class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template> <ng-template #notSeen><span *ngIf="!auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added or prioritized out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span> <span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
<span *ngIf="auditStatus.prioritized" class="badge badge-warning mr-1" i18n-ngbTooltip="Prioritized transaction tooltip" ngbTooltip="This transaction may have been prioritized out-of-band" placement="bottom" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span> <span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
</ng-container> </ng-container>
</td> </td>

View File

@ -42,6 +42,7 @@ interface AuditStatus {
seen?: boolean; seen?: boolean;
expected?: boolean; expected?: boolean;
added?: boolean; added?: boolean;
prioritized?: boolean;
delayed?: number; delayed?: number;
accelerated?: boolean; accelerated?: boolean;
conflict?: boolean; conflict?: boolean;
@ -317,13 +318,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchAudit ? this.apiService.getBlockAudit$(hash).pipe( fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
map(audit => { map(audit => {
const isAdded = audit.addedTxs.includes(txid); const isAdded = audit.addedTxs.includes(txid);
const isPrioritized = audit.prioritizedTxs.includes(txid);
const isAccelerated = audit.acceleratedTxs.includes(txid); const isAccelerated = audit.acceleratedTxs.includes(txid);
const isConflict = audit.fullrbfTxs.includes(txid); const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid); const isExpected = audit.template.some(tx => tx.txid === txid);
return { return {
seen: isExpected || !(isAdded || isConflict), seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected, expected: isExpected,
added: isAdded, added: isAdded,
prioritized: isPrioritized,
conflict: isConflict, conflict: isConflict,
accelerated: isAccelerated, accelerated: isAccelerated,
}; };

View File

@ -1,8 +1,8 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface'; import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg, TransactionStripped } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { MempoolInfo, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service'; import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';

View File

@ -208,6 +208,7 @@ export interface BlockExtended extends Block {
export interface BlockAudit extends BlockExtended { export interface BlockAudit extends BlockExtended {
missingTxs: string[], missingTxs: string[],
addedTxs: string[], addedTxs: string[],
prioritizedTxs: string[],
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[], fullrbfTxs: string[],
@ -230,7 +231,8 @@ export interface TransactionStripped {
rate?: number; // effective fee rate rate?: number; // effective fee rate
acc?: boolean; acc?: boolean;
flags?: number | null; flags?: number | null;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }

View File

@ -1,9 +1,10 @@
import { SafeResourceUrl } from '@angular/platform-browser'; import { SafeResourceUrl } from '@angular/platform-browser';
import { ILoadingIndicators } from '../services/state.service'; import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface'; import { Transaction } from './electrs.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface'; import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
export interface WebsocketResponse { export interface WebsocketResponse {
backend?: 'esplora' | 'electrum' | 'none';
block?: BlockExtended; block?: BlockExtended;
blocks?: BlockExtended[]; blocks?: BlockExtended[];
conversions?: any; conversions?: any;
@ -92,20 +93,8 @@ export interface MempoolInfo {
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
} }
export interface TransactionStripped {
txid: string;
fee: number;
vsize: number;
value: number;
acc?: boolean; // is accelerated?
rate?: number; // effective fee rate
flags?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}
// [txid, fee, vsize, value, rate, flags, acceleration?] // [txid, fee, vsize, value, rate, flags, acceleration?]
export type TransactionCompressed = [string, number, number, number, number, number, 1?]; export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
// [txid, rate, flags, acceleration?] // [txid, rate, flags, acceleration?]
export type MempoolDeltaChange = [string, number, number, (1|0)]; export type MempoolDeltaChange = [string, number, number, (1|0)];

View File

@ -1,8 +1,8 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, makeStateKey, TransferState } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@Injectable() @Injectable()

View File

@ -1,8 +1,8 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { filter, map, scan, shareReplay } from 'rxjs/operators';
@ -92,6 +92,7 @@ const defaultEnv: Env = {
export class StateService { export class StateService {
isBrowser: boolean = isPlatformBrowser(this.platformId); isBrowser: boolean = isPlatformBrowser(this.platformId);
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false; isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
network = ''; network = '';
lightning = false; lightning = false;
blockVSize: number; blockVSize: number;
@ -99,6 +100,7 @@ export class StateService {
latestBlockHeight = -1; latestBlockHeight = -1;
blocks: BlockExtended[] = []; blocks: BlockExtended[] = [];
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1); lightningChanged$ = new ReplaySubject<boolean>(1);
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]); blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
@ -257,6 +259,10 @@ export class StateService {
const rateUnitPreference = this.storageService.getValue('rate-unit-preference'); const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb'); this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
this.backend$.subscribe(backend => {
this.backend = backend;
});
} }
setNetworkBasedonUrl(url: string) { setNetworkBasedonUrl(url: string) {

View File

@ -62,6 +62,7 @@ export class WebsocketService {
if (theInitData.body.blocks) { if (theInitData.body.blocks) {
theInitData.body.blocks = theInitData.body.blocks.reverse(); theInitData.body.blocks = theInitData.body.blocks.reverse();
} }
this.stateService.backend$.next(theInitData.backend);
this.stateService.isLoadingWebSocket$.next(false); this.stateService.isLoadingWebSocket$.next(false);
this.handleResponse(theInitData.body); this.handleResponse(theInitData.body);
this.startSubscription(false, true); this.startSubscription(false, true);
@ -290,6 +291,10 @@ export class WebsocketService {
handleResponse(response: WebsocketResponse) { handleResponse(response: WebsocketResponse) {
let reinitBlocks = false; let reinitBlocks = false;
if (response.backend) {
this.stateService.backend$.next(response.backend);
}
if (response.blocks && response.blocks.length) { if (response.blocks && response.blocks.length) {
const blocks = response.blocks; const blocks = response.blocks;
this.stateService.resetBlocks(blocks); this.stateService.resetBlocks(blocks);

View File

@ -1,4 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface"; import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
import { TransactionStripped } from "../interfaces/node-api.interface";
export function isMobile(): boolean { export function isMobile(): boolean {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
@ -164,7 +165,8 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
value: tx[3], value: tx[3],
rate: tx[4], rate: tx[4],
flags: tx[5], flags: tx[5],
acc: !!tx[6], time: tx[6],
acc: !!tx[7],
}; };
} }

View File

@ -7,5 +7,5 @@ if (environment.production) {
enableProdMode(); enableProdMode();
} }
export { AppServerModule } from './app/app.server.module'; export { AppServerModule } from './app/app.module.server';
export { renderModule } from '@angular/platform-server'; export { renderModule } from '@angular/platform-server';

View File

@ -32,19 +32,19 @@ const githubSecret = process.env.GITHUB_TOKEN;
const CONFIG_FILE_NAME = 'mempool-frontend-config.json'; const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
let configContent = {}; let configContent = {};
var PATH; var ASSETS_PATH;
if (process.argv[2]) { if (process.argv[2]) {
PATH = process.argv[2]; ASSETS_PATH = process.argv[2];
PATH += PATH.endsWith("/") ? "" : "/" ASSETS_PATH += ASSETS_PATH.endsWith("/") ? "" : "/"
PATH = path.resolve(path.normalize(PATH)); ASSETS_PATH = path.resolve(path.normalize(ASSETS_PATH));
console.log(`[sync-assets] using PATH ${PATH}`); console.log(`[sync-assets] using ASSETS_PATH ${ASSETS_PATH}`);
if (!fs.existsSync(PATH)){ if (!fs.existsSync(ASSETS_PATH)){
console.log(`${LOG_TAG} ${PATH} does not exist, creating`); console.log(`${LOG_TAG} ${ASSETS_PATH} does not exist, creating`);
fs.mkdirSync(PATH, { recursive: true }); fs.mkdirSync(ASSETS_PATH, { recursive: true });
} }
} }
if (!PATH) { if (!ASSETS_PATH) {
throw new Error('Resource path argument is not set'); throw new Error('Resource path argument is not set');
} }
@ -125,7 +125,8 @@ function downloadMiningPoolLogos$() {
if (verbose) { if (verbose) {
console.log(`${LOG_TAG} Processing ${poolLogo.name}`); console.log(`${LOG_TAG} Processing ${poolLogo.name}`);
} }
const filePath = `${PATH}/mining-pools/${poolLogo.name}`; console.log(`${ASSETS_PATH}/mining-pools/${poolLogo.name}`);
const filePath = `${ASSETS_PATH}/mining-pools/${poolLogo.name}`;
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const localHash = getLocalHash(filePath); const localHash = getLocalHash(filePath);
if (verbose) { if (verbose) {
@ -152,7 +153,7 @@ function downloadMiningPoolLogos$() {
} }
} else { } else {
console.log(`${LOG_TAG} \t\t${poolLogo.name} is missing, downloading...`); console.log(`${LOG_TAG} \t\t${poolLogo.name} is missing, downloading...`);
const miningPoolsDir = `${PATH}/mining-pools/`; const miningPoolsDir = `${ASSETS_PATH}/mining-pools/`;
if (!fs.existsSync(miningPoolsDir)){ if (!fs.existsSync(miningPoolsDir)){
fs.mkdirSync(miningPoolsDir, { recursive: true }); fs.mkdirSync(miningPoolsDir, { recursive: true });
} }
@ -219,7 +220,7 @@ function downloadPromoVideoSubtiles$() {
if (verbose) { if (verbose) {
console.log(`${LOG_TAG} Processing ${language.name}`); console.log(`${LOG_TAG} Processing ${language.name}`);
} }
const filePath = `${PATH}/promo-video/${language.name}`; const filePath = `${ASSETS_PATH}/promo-video/${language.name}`;
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
if (verbose) { if (verbose) {
console.log(`${LOG_TAG} \t${language.name} remote promo video hash ${language.sha}`); console.log(`${LOG_TAG} \t${language.name} remote promo video hash ${language.sha}`);
@ -245,7 +246,7 @@ function downloadPromoVideoSubtiles$() {
} }
} else { } else {
console.log(`${LOG_TAG} \t\t${language.name} is missing, downloading`); console.log(`${LOG_TAG} \t\t${language.name} is missing, downloading`);
const promoVideosDir = `${PATH}/promo-video/`; const promoVideosDir = `${ASSETS_PATH}/promo-video/`;
if (!fs.existsSync(promoVideosDir)){ if (!fs.existsSync(promoVideosDir)){
fs.mkdirSync(promoVideosDir, { recursive: true }); fs.mkdirSync(promoVideosDir, { recursive: true });
} }
@ -313,7 +314,7 @@ function downloadPromoVideo$() {
if (item.name !== 'promo.mp4') { if (item.name !== 'promo.mp4') {
continue; continue;
} }
const filePath = `${PATH}/promo-video/mempool-promo.mp4`; const filePath = `${ASSETS_PATH}/promo-video/mempool-promo.mp4`;
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const localHash = getLocalHash(filePath); const localHash = getLocalHash(filePath);
@ -373,16 +374,16 @@ if (configContent.BASE_MODULE && configContent.BASE_MODULE === 'liquid') {
const testnetAssetsMinimalJsonUrl = 'https://raw.githubusercontent.com/Blockstream/asset_registry_testnet_db/master/index.minimal.json'; const testnetAssetsMinimalJsonUrl = 'https://raw.githubusercontent.com/Blockstream/asset_registry_testnet_db/master/index.minimal.json';
console.log(`${LOG_TAG} Downloading assets`); console.log(`${LOG_TAG} Downloading assets`);
download(`${PATH}/assets.json`, assetsJsonUrl); download(`${ASSETS_PATH}/assets.json`, assetsJsonUrl);
console.log(`${LOG_TAG} Downloading assets minimal`); console.log(`${LOG_TAG} Downloading assets minimal`);
download(`${PATH}/assets.minimal.json`, assetsMinimalJsonUrl); download(`${ASSETS_PATH}/assets.minimal.json`, assetsMinimalJsonUrl);
console.log(`${LOG_TAG} Downloading testnet assets`); console.log(`${LOG_TAG} Downloading testnet assets`);
download(`${PATH}/assets-testnet.json`, testnetAssetsJsonUrl); download(`${ASSETS_PATH}/assets-testnet.json`, testnetAssetsJsonUrl);
console.log(`${LOG_TAG} Downloading testnet assets minimal`); console.log(`${LOG_TAG} Downloading testnet assets minimal`);
download(`${PATH}/assets-testnet.minimal.json`, testnetAssetsMinimalJsonUrl); download(`${ASSETS_PATH}/assets-testnet.minimal.json`, testnetAssetsMinimalJsonUrl);
} else { } else {
if (verbose) { if (verbose) {
console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (currently ${configContent.BASE_MODULE}), skipping downloading assets`); console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (currently ${configContent.BASE_MODULE}), skipping downloading assets`);

View File

@ -7,7 +7,7 @@
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"module": "ES2020", "module": "ES2022",
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",
@ -15,7 +15,7 @@
"node_modules/@types" "node_modules/@types"
], ],
"lib": [ "lib": [
"ES2018", "ES2022",
"dom", "dom",
"dom.iterable" "dom.iterable"
] ]

View File

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