Merge branch 'master' into simon/connectivity-ranking-title-fix

This commit is contained in:
wiz 2023-07-13 15:44:31 +09:00 committed by GitHub
commit a6dc4fa38c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 512 additions and 192 deletions

View File

@ -29,7 +29,9 @@
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"RUST_GBT": false, "RUST_GBT": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6 "DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View File

@ -7570,7 +7570,7 @@
"name": "gbt", "name": "gbt",
"version": "0.1.0", "version": "0.1.0",
"hasInstallScript": true, "hasInstallScript": true,
"devDependencies": { "dependencies": {
"@napi-rs/cli": "^2.16.1" "@napi-rs/cli": "^2.16.1"
}, },
"engines": { "engines": {

View File

@ -30,7 +30,9 @@
"RUST_GBT": false, "RUST_GBT": false,
"CPFP_INDEXING": true, "CPFP_INDEXING": true,
"MAX_BLOCKS_BULK_QUERY": 999, "MAX_BLOCKS_BULK_QUERY": 999,
"DISK_CACHE_BLOCK_INTERVAL": 999 "DISK_CACHE_BLOCK_INTERVAL": 999,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
@ -120,4 +122,4 @@
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
} }
} }

View File

@ -44,6 +44,8 @@ describe('Mempool Backend Config', () => {
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6, DISK_CACHE_BLOCK_INTERVAL: 6,
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
ALLOW_UNREACHABLE: true,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });

View File

@ -723,12 +723,7 @@ class BitcoinRoutes {
private async $postTransaction(req: Request, res: Response) { private async $postTransaction(req: Request, res: Response) {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
try { try {
let rawTx; const rawTx = Common.getTransactionFromRequest(req, false);
if (typeof req.body === 'object') {
rawTx = Object.keys(req.body)[0];
} else {
rawTx = req.body;
}
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
@ -739,12 +734,8 @@ class BitcoinRoutes {
private async $postTransactionForm(req: Request, res: Response) { private async $postTransactionForm(req: Request, res: Response) {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
const matches = /tx=([a-z0-9]+)/.exec(req.body);
let txHex = '';
if (matches && matches[1]) {
txHex = matches[1];
}
try { try {
const txHex = Common.getTransactionFromRequest(req, true);
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {

View File

@ -25,6 +25,7 @@ import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmen
import PricesRepository from '../repositories/PricesRepository'; import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import chainTips from './chain-tips'; import chainTips from './chain-tips';
import websocketHandler from './websocket-handler';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@ -686,6 +687,8 @@ class Blocks {
this.updateTimerProgress(timer, `reindexed difficulty adjustments`); this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining); logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
indexer.reindex(); indexer.reindex();
websocketHandler.handleReorg();
} }
} }

View File

@ -1,3 +1,5 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
@ -86,19 +88,19 @@ export class Common {
const match = spendMap.get(`${vin.txid}:${vin.vout}`); const match = spendMap.get(`${vin.txid}:${vin.vout}`);
if (match && match.txid !== tx.txid) { if (match && match.txid !== tx.txid) {
replaced.add(match); replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
}
} }
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
} }
if (replaced.size) { if (replaced.size) {
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx }; matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
} }
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const vin of tx.vin) {
const key = `${vin.txid}:${vin.vout}`;
if (spendMap.get(key)?.txid === tx.txid) {
spendMap.delete(key);
}
}
} }
return matches; return matches;
} }
@ -511,6 +513,115 @@ export class Common {
static getNthPercentile(n: number, sortedDistribution: any[]): any { static getNthPercentile(n: number, sortedDistribution: any[]): any {
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))]; return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
} }
static getTransactionFromRequest(req: Request, form: boolean): string {
let rawTx: any = typeof req.body === 'object' && form
? Object.values(req.body)[0] as any
: req.body;
if (typeof rawTx !== 'string') {
throw Object.assign(new Error('Non-string request body'), { code: -1 });
}
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = form ? /^txHash=((?:[a-fA-F0-9]{2})+)$/ : /^((?:[a-fA-F0-9]{2})+)$/;
const matches = reg.exec(rawTx);
if (!matches || !matches[1]) {
throw Object.assign(new Error('Non-hex request body'), { code: -2 });
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this.validateTransactionHex(matches[1].toLowerCase());
}
private static validateTransactionHex(txhex: string): string {
// Do not mutate txhex
// We assume txhex to be valid hex (output of getTransactionFromRequest above)
// Check 1: Valid transaction parse
let tx: bitcoinjs.Transaction;
try {
tx = bitcoinjs.Transaction.fromHex(txhex);
} catch(e) {
throw Object.assign(new Error('Invalid transaction (could not parse)'), { code: -4 });
}
// Check 2: Simple size check
if (tx.weight() > config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT) {
throw Object.assign(new Error(`Transaction too large (max ${config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT} weight units)`), { code: -3 });
}
// Check 3: Check unreachable script in taproot (if not allowed)
if (!config.MEMPOOL.ALLOW_UNREACHABLE) {
tx.ins.forEach(input => {
const witness = input.witness;
// See BIP 341: Script validation rules
const hasAnnex = witness.length >= 2 &&
witness[witness.length - 1][0] === 0x50;
const scriptSpendMinLength = hasAnnex ? 3 : 2;
const maybeScriptSpend = witness.length >= scriptSpendMinLength;
if (maybeScriptSpend) {
const controlBlock = witness[witness.length - scriptSpendMinLength + 1];
if (controlBlock.length === 0 || !this.isValidLeafVersion(controlBlock[0])) {
// Skip this input, it's not taproot
return;
}
// Definitely taproot. Get script
const script = witness[witness.length - scriptSpendMinLength];
const decompiled = bitcoinjs.script.decompile(script);
if (!decompiled || decompiled.length < 2) {
// Skip this input
return;
}
// Iterate up to second last (will look ahead 1 item)
for (let i = 0; i < decompiled.length - 1; i++) {
const first = decompiled[i];
const second = decompiled[i + 1];
if (
first === bitcoinjs.opcodes.OP_FALSE &&
second === bitcoinjs.opcodes.OP_IF
) {
throw Object.assign(new Error('Unreachable taproot scripts not allowed'), { code: -5 });
}
}
}
})
}
// Pass through the input string untouched
return txhex;
}
private static isValidLeafVersion(leafVersion: number): boolean {
// See Note 7 in BIP341
// https://github.com/bitcoin/bips/blob/66a1a8151021913047934ebab3f8883f2f8ca75b/bip-0341.mediawiki#cite_note-7
// "What constraints are there on the leaf version?"
// Must be an integer between 0 and 255
// Since we're parsing a byte
if (Math.floor(leafVersion) !== leafVersion || leafVersion < 0 || leafVersion > 255) {
return false;
}
// "the leaf version cannot be odd"
if ((leafVersion & 0x01) === 1) {
return false;
}
// "The values that comply to this rule are
// the 32 even values between 0xc0 and 0xfe
if (leafVersion >= 0xc0 && leafVersion <= 0xfe) {
return true;
}
// and also 0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe."
if ([0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe].includes(leafVersion)) {
return true;
}
// Otherwise, invalid
return false;
}
} }
/** /**

View File

@ -55,7 +55,7 @@ class RbfCache {
if (tree) { if (tree) {
tree.interval = newTime - tree?.time; tree.interval = newTime - tree?.time;
replacedTrees.push(tree); replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf; fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf;
} }
} }
} else { } else {

View File

@ -333,6 +333,40 @@ class WebsocketHandler {
}); });
} }
handleReorg(): void {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
const da = difficultyAdjustment.getDifficultyAdjustment();
// update init data
this.updateSocketDataFields({
'blocks': blocks.getBlocks(),
'da': da?.previousTime ? da : undefined,
});
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
const response = {};
if (client['want-blocks']) {
response['blocks'] = this.socketData['blocks'];
}
if (client['want-stats']) {
response['da'] = this.socketData['da'];
}
if (Object.keys(response).length) {
const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse);
}
});
}
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
if (!this.wss) { if (!this.wss) {

View File

@ -35,6 +35,8 @@ interface IConfig {
CPFP_INDEXING: boolean; CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number; MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number; DISK_CACHE_BLOCK_INTERVAL: number;
MAX_PUSH_TX_SIZE_WEIGHT: number;
ALLOW_UNREACHABLE: boolean;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@ -165,6 +167,8 @@ const defaults: IConfig = {
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6, 'DISK_CACHE_BLOCK_INTERVAL': 6,
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
'ALLOW_UNREACHABLE': true,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',

View File

@ -6,6 +6,7 @@ import logger from './logger';
import bitcoinClient from './api/bitcoin/bitcoin-client'; import bitcoinClient from './api/bitcoin/bitcoin-client';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import PricesRepository from './repositories/PricesRepository'; import PricesRepository from './repositories/PricesRepository';
import config from './config';
export interface CoreIndex { export interface CoreIndex {
name: string; name: string;
@ -72,7 +73,7 @@ class Indexer {
return; return;
} }
if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) { if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
this.tasksRunning.push(task); this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) { if (priceUpdater.historyInserted === false || lastestPriceId === null) {

View File

@ -401,7 +401,7 @@ class BlocksRepository {
/** /**
* Get average block health for all blocks for a single pool * Get average block health for all blocks for a single pool
*/ */
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> { public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number | null> {
const params: any[] = []; const params: any[] = [];
const query = ` const query = `
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
@ -413,8 +413,8 @@ class BlocksRepository {
try { try {
const [rows] = await DB.query(query, params); const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].avg_match_rate) { if (!rows[0] || rows[0].avg_match_rate == null) {
return 0; return null;
} }
return Math.round(rows[0].avg_match_rate * 100) / 100; return Math.round(rows[0].avg_match_rate * 100) / 100;
} catch (e) { } catch (e) {

View File

@ -269,7 +269,11 @@ class NetworkSyncService {
} }
private async $scanForClosedChannels(): Promise<void> { private async $scanForClosedChannels(): Promise<void> {
if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) { let currentBlockHeight = blocks.getCurrentBlockHeight();
if (config.MEMPOOL.ENABLED === false) { // https://github.com/mempool/mempool/issues/3582
currentBlockHeight = await bitcoinApi.$getBlockHeightTip();
}
if (this.closedChannelsScanBlock === currentBlockHeight) {
logger.debug(`We've already scan closed channels for this block, skipping.`); logger.debug(`We've already scan closed channels for this block, skipping.`);
return; return;
} }
@ -305,7 +309,7 @@ class NetworkSyncService {
} }
} }
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight(); this.closedChannelsScanBlock = currentBlockHeight;
logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln); logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln);
} catch (e) { } catch (e) {
logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);

View File

@ -29,6 +29,8 @@
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__, "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__, "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__, "DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__" "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
}, },
@ -126,4 +128,4 @@
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__", "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__" "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
} }
} }

View File

@ -32,6 +32,9 @@ __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6} __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@ -161,6 +164,8 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" me
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json

View File

@ -112,7 +112,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
this.error = error; this.error = error;
}); });
this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block))); this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.bsqPrice$ this.stateService.bsqPrice$
.subscribe((bsqPrice) => { .subscribe((bsqPrice) => {

View File

@ -27,7 +27,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
} }
ngOnInit() { ngOnInit() {
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
} }
ngOnChanges() { ngOnChanges() {

View File

@ -3,7 +3,7 @@
<span i18n="shared.address">Address</span> <span i18n="shared.address">Address</span>
</app-preview-title> </app-preview-title>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md table-col">
<div class="row d-flex justify-content-between"> <div class="row d-flex justify-content-between">
<div class="title-wrapper"> <div class="title-wrapper">
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1> <h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>

View File

@ -20,6 +20,11 @@
margin-right: 15px; margin-right: 15px;
} }
.table-col {
max-width: calc(100% - 470px);
overflow: hidden;
}
.table { .table {
font-size: 32px; font-size: 32px;
margin-top: 48px; margin-top: 48px;

View File

@ -3,7 +3,7 @@
<span i18n="shared.block-title">Block</span> <span i18n="shared.block-title">Block</span>
</app-preview-title> </app-preview-title>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm table-col">
<div class="row"> <div class="row">
<div class="block-titles"> <div class="block-titles">
<h1 class="title"> <h1 class="title">

View File

@ -44,6 +44,11 @@
} }
} }
.table-col {
max-width: calc(100% - 470px);
overflow: hidden;
}
.chart-container { .chart-container {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;

View File

@ -14,6 +14,7 @@ import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils'; import { detectWebGL } from '../../shared/graphs.utils';
import { PriceService, Price } from '../../services/price.service'; import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
@ -72,6 +73,7 @@ export class BlockComponent implements OnInit, OnDestroy {
auditSubscription: Subscription; auditSubscription: Subscription;
keyNavigationSubscription: Subscription; keyNavigationSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription; networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription; queryParamsSubscription: Subscription;
nextBlockSubscription: Subscription = undefined; nextBlockSubscription: Subscription = undefined;
@ -99,6 +101,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService, private apiService: ApiService,
private priceService: PriceService, private priceService: PriceService,
private cacheService: CacheService,
) { ) {
this.webGlEnabled = detectWebGL(); this.webGlEnabled = detectWebGL();
} }
@ -128,19 +131,27 @@ export class BlockComponent implements OnInit, OnDestroy {
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0) map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
); );
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
this.loadedCacheBlock(block);
});
this.blocksSubscription = this.stateService.blocks$ this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => { .subscribe((blocks) => {
this.latestBlock = block; this.latestBlock = blocks[0];
this.latestBlocks.unshift(block); this.latestBlocks = blocks;
this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
this.setNextAndPreviousBlockLink(); this.setNextAndPreviousBlockLink();
if (block.id === this.blockHash) { for (const block of blocks) {
this.block = block; if (block.id === this.blockHash) {
block.extras.minFee = this.getMinBlockFee(block); this.block = block;
block.extras.maxFee = this.getMaxBlockFee(block); block.extras.minFee = this.getMinBlockFee(block);
if (block?.extras?.reward != undefined) { block.extras.maxFee = this.getMaxBlockFee(block);
this.fees = block.extras.reward / 100000000 - this.blockSubsidy; if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
}
} else if (block.height === this.block?.height) {
this.block.stale = true;
this.block.canonical = block.id;
} }
} }
}); });
@ -254,6 +265,13 @@ export class BlockComponent implements OnInit, OnDestroy {
this.transactionsError = null; this.transactionsError = null;
this.isLoadingOverview = true; this.isLoadingOverview = true;
this.overviewError = null; this.overviewError = null;
const cachedBlock = this.cacheService.getCachedBlock(block.height);
if (!cachedBlock) {
this.cacheService.loadBlock(block.height);
} else {
this.loadedCacheBlock(cachedBlock);
}
}), }),
throttleTime(300, asyncScheduler, { leading: true, trailing: true }), throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
shareReplay(1) shareReplay(1)
@ -459,6 +477,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.auditSubscription?.unsubscribe(); this.auditSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe();
this.blocksSubscription?.unsubscribe(); this.blocksSubscription?.unsubscribe();
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe(); this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe();
this.timeLtrSubscription?.unsubscribe(); this.timeLtrSubscription?.unsubscribe();
@ -679,4 +698,11 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
return 0; return 0;
} }
loadedCacheBlock(block: BlockExtended): void {
if (this.block && block.height === this.block.height && block.id !== this.block.id) {
this.block.stale = true;
this.block.canonical = block.id;
}
}
} }

View File

@ -36,11 +36,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
markHeight: number; markHeight: number;
chainTip: number; chainTip: number;
pendingMarkBlock: { animate: boolean, newBlockFromLeft: boolean };
blocksSubscription: Subscription; blocksSubscription: Subscription;
blockPageSubscription: Subscription; blockPageSubscription: Subscription;
networkSubscription: Subscription; networkSubscription: Subscription;
tabHiddenSubscription: Subscription; tabHiddenSubscription: Subscription;
markBlockSubscription: Subscription; markBlockSubscription: Subscription;
txConfirmedSubscription: Subscription;
loadingBlocks$: Observable<boolean>; loadingBlocks$: Observable<boolean>;
blockStyles = []; blockStyles = [];
emptyBlockStyles = []; emptyBlockStyles = [];
@ -82,7 +84,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT); this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) { if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
@ -104,31 +105,22 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
if (!this.static) { if (!this.static) {
this.blocksSubscription = this.stateService.blocks$ this.blocksSubscription = this.stateService.blocks$
.subscribe(([block, txConfirmed]) => { .subscribe((blocks) => {
if (this.blocks.some((b) => b.height === block.height)) { if (!blocks?.length) {
return; return;
} }
const latestHeight = blocks[0].height;
const animate = this.chainTip != null && latestHeight > this.chainTip;
if (this.blocks.length && block.height !== this.blocks[0].height + 1) { for (const block of blocks) {
this.blocks = []; block.extras.minFee = this.getMinBlockFee(block);
this.blocksFilled = false; block.extras.maxFee = this.getMaxBlockFee(block);
} }
block.extras.minFee = this.getMinBlockFee(block); this.blocks = blocks;
block.extras.maxFee = this.getMaxBlockFee(block);
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
if (txConfirmed && block.height > this.chainTip) {
this.markHeight = block.height;
this.moveArrowToPosition(true, true);
} else {
this.moveArrowToPosition(true, false);
}
this.blockStyles = []; this.blockStyles = [];
if (this.blocksFilled && block.height > this.chainTip) { if (animate) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
setTimeout(() => { setTimeout(() => {
this.blockStyles = []; this.blockStyles = [];
@ -139,13 +131,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
} }
if (this.blocks.length === this.dynamicBlocksAmount) { this.chainTip = latestHeight;
this.blocksFilled = true;
}
this.chainTip = Math.max(this.chainTip, block.height); if (this.pendingMarkBlock) {
this.moveArrowToPosition(this.pendingMarkBlock.animate, this.pendingMarkBlock.newBlockFromLeft);
this.pendingMarkBlock = null;
}
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txid, block]) => {
if (txid) {
this.markHeight = block.height;
this.moveArrowToPosition(true, true);
} else {
this.moveArrowToPosition(true, false);
}
})
} else { } else {
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => { this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
if (block.height <= this.height && block.height > this.height - this.count) { if (block.height <= this.height && block.height > this.height - this.count) {
@ -164,9 +166,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck(); this.cd.markForCheck();
}); });
if (this.static) { if (this.static) {
this.updateStaticBlocks(); this.updateStaticBlocks();
} }
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@ -190,6 +192,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.blockPageSubscription) { if (this.blockPageSubscription) {
this.blockPageSubscription.unsubscribe(); this.blockPageSubscription.unsubscribe();
} }
if (this.txConfirmedSubscription) {
this.txConfirmedSubscription.unsubscribe();
}
this.networkSubscription.unsubscribe(); this.networkSubscription.unsubscribe();
this.tabHiddenSubscription.unsubscribe(); this.tabHiddenSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe(); this.markBlockSubscription.unsubscribe();
@ -202,6 +207,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.arrowVisible = false; this.arrowVisible = false;
return; return;
} }
if (this.chainTip == null) {
this.pendingMarkBlock = { animate, newBlockFromLeft };
}
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
if (blockindex > -1) { if (blockindex > -1) {
if (!animate) { if (!animate) {

View File

@ -82,12 +82,12 @@ export class BlocksList implements OnInit {
), ),
this.stateService.blocks$ this.stateService.blocks$
.pipe( .pipe(
switchMap((block) => { switchMap((blocks) => {
if (block[0].height <= this.lastBlockHeight) { if (blocks[0].height <= this.lastBlockHeight) {
return [null]; // Return an empty stream so the last pipe is not executed return [null]; // Return an empty stream so the last pipe is not executed
} }
this.lastBlockHeight = block[0].height; this.lastBlockHeight = blocks[0].height;
return [block]; return blocks;
}) })
) )
]) ])

View File

@ -1,3 +1,3 @@
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'"> <span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}% &lrm;{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
</span> </span>

View File

@ -39,13 +39,10 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
}) })
).subscribe(); ).subscribe();
this.blocksSubscription = this.stateService.blocks$ this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => { .subscribe((blocks) => {
if (block) { this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]); this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
// using block-reported times, so ensure they are sorted chronologically this.updateSegments();
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
}
}); });
} }

View File

@ -60,14 +60,11 @@ export class ClockComponent implements OnInit {
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']); this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
this.blocksSubscription = this.stateService.blocks$ this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => { .subscribe((blocks) => {
if (block) { this.blocks = blocks.slice(0, 16);
this.blocks.unshift(block); if (this.blocks[this.blockIndex]) {
this.blocks = this.blocks.slice(0, 16); this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
if (this.blocks[this.blockIndex]) { this.cd.markForCheck();
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
this.cd.markForCheck();
}
} }
}); });

View File

@ -38,11 +38,12 @@ export class DifficultyMiningComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([ this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)), this.stateService.blocks$,
this.stateService.difficultyAdjustment$, this.stateService.difficultyAdjustment$,
]) ])
.pipe( .pipe(
map(([block, da]) => { map(([blocks, da]) => {
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
let colorAdjustments = '#ffffff66'; let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) { if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49'; colorAdjustments = '#3bcc49';
@ -63,7 +64,7 @@ export class DifficultyMiningComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66'; colorPreviousAdjustments = '#ffffff66';
} }
const blocksUntilHalving = 210000 - (block.height % 210000); const blocksUntilHalving = 210000 - (maxHeight % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const data = { const data = {

View File

@ -67,11 +67,12 @@ export class DifficultyComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([ this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)), this.stateService.blocks$,
this.stateService.difficultyAdjustment$, this.stateService.difficultyAdjustment$,
]) ])
.pipe( .pipe(
map(([block, da]) => { map(([blocks, da]) => {
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
let colorAdjustments = '#ffffff66'; let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) { if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49'; colorAdjustments = '#3bcc49';
@ -92,7 +93,7 @@ export class DifficultyComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66'; colorPreviousAdjustments = '#ffffff66';
} }
const blocksUntilHalving = 210000 - (block.height % 210000); const blocksUntilHalving = 210000 - (maxHeight % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000); const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH; const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks); const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);

View File

@ -109,6 +109,14 @@ export class HashrateChartComponent implements OnInit {
tap((response: any) => { tap((response: any) => {
const data = response.body; const data = response.body;
// always include the latest difficulty
if (data.difficulty.length && data.difficulty[data.difficulty.length - 1].difficulty !== data.currentDifficulty) {
data.difficulty.push({
timestamp: Date.now() / 1000,
difficulty: data.currentDifficulty
});
}
// We generate duplicated data point so the tooltip works nicely // We generate duplicated data point so the tooltip works nicely
const diffFixed = []; const diffFixed = [];
let diffIndex = 1; let diffIndex = 1;
@ -137,6 +145,14 @@ export class HashrateChartComponent implements OnInit {
++diffIndex; ++diffIndex;
} }
while (diffIndex <= data.difficulty.length) {
diffFixed.push({
timestamp: data.difficulty[diffIndex - 1].time,
difficulty: data.difficulty[diffIndex - 1].difficulty
});
diffIndex++;
}
let maResolution = 15; let maResolution = 15;
const hashrateMa = []; const hashrateMa = [];
for (let i = maResolution - 1; i < data.hashrates.length; ++i) { for (let i = maResolution - 1; i < data.hashrates.length; ++i) {

View File

@ -124,7 +124,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
) )
.pipe( .pipe(
switchMap(() => combineLatest([ switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)), this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$ this.stateService.mempoolBlocks$
.pipe( .pipe(
map((mempoolBlocks) => { map((mempoolBlocks) => {
@ -186,8 +186,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.blockSubscription = this.stateService.blocks$ this.blockSubscription = this.stateService.blocks$.pipe(map((blocks) => blocks[0]))
.subscribe(([block]) => { .subscribe((block) => {
if (!block) {
return;
}
if (this.chainTip === -1) { if (this.chainTip === -1) {
this.animateEntry = block.height === this.stateService.latestBlockHeight; this.animateEntry = block.height === this.stateService.latestBlockHeight;
} else { } else {
@ -221,8 +224,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]); this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
} else { } else {
this.stateService.blocks$ this.stateService.blocks$
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT)) .pipe(map((blocks) => blocks[0]))
.subscribe(([block]) => { .subscribe((block) => {
if (this.stateService.latestBlockHeight === block.height) { if (this.stateService.latestBlockHeight === block.height) {
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }}); this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
} }
@ -297,7 +300,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
while (blocks.length > blocksAmount) { while (blocks.length > blocksAmount) {
const block = blocks.pop(); const block = blocks.pop();
if (!this.count) { if (!this.count) {
const lastBlock = blocks[blocks.length - 1]; const lastBlock = blocks[0];
lastBlock.blockSize += block.blockSize; lastBlock.blockSize += block.blockSize;
lastBlock.blockVSize += block.blockVSize; lastBlock.blockVSize += block.blockVSize;
lastBlock.nTx += block.nTx; lastBlock.nTx += block.nTx;
@ -308,7 +311,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
if (blocks.length) { if (blocks.length) {
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize; blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
} }
return blocks; return blocks;
} }

View File

@ -37,7 +37,7 @@ export class PoolComponent implements OnInit {
auditAvailable = false; auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height); loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[0]?.height);
constructor( constructor(
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
@ -68,7 +68,7 @@ export class PoolComponent implements OnInit {
return this.apiService.getPoolStats$(slug); return this.apiService.getPoolStats$(slug);
}), }),
tap(() => { tap(() => {
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height); this.loadMoreSubject.next(this.blocks[0]?.height);
}), }),
map((poolStats) => { map((poolStats) => {
this.seoService.setTitle(poolStats.pool.name); this.seoService.setTitle(poolStats.pool.name);
@ -91,7 +91,7 @@ export class PoolComponent implements OnInit {
if (this.slug === undefined) { if (this.slug === undefined) {
return []; return [];
} }
return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height); return this.apiService.getPoolBlocks$(this.slug, this.blocks[0]?.height);
}), }),
tap((newBlocks) => { tap((newBlocks) => {
this.blocks = this.blocks.concat(newBlocks); this.blocks = this.blocks.concat(newBlocks);
@ -237,7 +237,7 @@ export class PoolComponent implements OnInit {
} }
loadMore() { loadMore() {
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height); this.loadMoreSubject.next(this.blocks[0]?.height);
} }
trackByBlock(index: number, block: BlockExtended) { trackByBlock(index: number, block: BlockExtended) {

View File

@ -32,6 +32,7 @@
<tr> <tr>
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td> <td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
<td> <td>
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span> <span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template> <ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> <span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
import { RbfInfo } from '../../interfaces/node-api.interface'; import { RbfTree } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-rbf-timeline-tooltip', selector: 'app-rbf-timeline-tooltip',
@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface';
styleUrls: ['./rbf-timeline-tooltip.component.scss'], styleUrls: ['./rbf-timeline-tooltip.component.scss'],
}) })
export class RbfTimelineTooltipComponent implements OnChanges { export class RbfTimelineTooltipComponent implements OnChanges {
@Input() rbfInfo: RbfInfo | void; @Input() rbfInfo: RbfTree | null;
@Input() cursorPosition: { x: number, y: number }; @Input() cursorPosition: { x: number, y: number };
tooltipPosition = null; tooltipPosition = null;

View File

@ -15,14 +15,15 @@
</div> </div>
<div class="nodes"> <div class="nodes">
<ng-container *ngFor="let cell of timeline; let i = index;"> <ng-container *ngFor="let cell of timeline; let i = index;">
<ng-container *ngIf="cell.replacement; else nonNode"> <ng-container *ngIf="cell.replacement?.tx; else nonNode">
<div class="node" <div class="node"
[id]="'node-'+cell.replacement.tx.txid" [id]="'node-'+cell.replacement.tx.txid"
[class.selected]="txid === cell.replacement.tx.txid" [class.selected]="txid === cell.replacement.tx.txid"
[class.mined]="cell.replacement.tx.mined" [class.mined]="cell.replacement.tx.mined"
[class.first-node]="cell.first" [class.first-node]="cell.first"
> >
<div class="track"></div> <div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
<a class="shape-border" <a class="shape-border"
[class.rbf]="cell.replacement.tx.rbf" [class.rbf]="cell.replacement.tx.rbf"
[routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]" [routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
@ -36,14 +37,14 @@
</ng-container> </ng-container>
<ng-template #nonNode> <ng-template #nonNode>
<ng-container [ngSwitch]="cell.connector"> <ng-container [ngSwitch]="cell.connector">
<div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div> <div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div> <div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="node-spacer" *ngSwitchDefault></div> <div class="node-spacer" *ngSwitchDefault></div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-container *ngIf="i < timeline.length - 1"> <ng-container *ngIf="i < timeline.length - 1">
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer"> <div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
<div class="track"></div> <div class="track" [class.fullrbf]="cell.fullRbf"></div>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@ -83,15 +83,26 @@
transform: translateY(-50%); transform: translateY(-50%);
background: #105fb0; background: #105fb0;
border-radius: 5px; border-radius: 5px;
&.left {
right: 50%;
}
&.right {
left: 50%;
}
&.fullrbf {
background: #1bd8f4;
}
} }
&.first-node { &.first-node {
.track { .track.left {
left: 50%; display: none;
} }
} }
&:last-child { &:last-child {
.track { .track.right {
right: 50%; display: none;
} }
} }
} }
@ -177,11 +188,17 @@
height: 108px; height: 108px;
bottom: 50%; bottom: 50%;
border-right: solid 10px #105fb0; border-right: solid 10px #105fb0;
&.fullrbf {
border-right: solid 10px #1bd8f4;
}
} }
.corner { .corner {
border-bottom: solid 10px #105fb0; border-bottom: solid 10px #105fb0;
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
&.fullrbf {
border-bottom: solid 10px #1bd8f4;
}
} }
} }
} }

View File

@ -1,15 +1,20 @@
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface'; import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
type Connector = 'pipe' | 'corner'; type Connector = 'pipe' | 'corner';
interface TimelineCell { interface TimelineCell {
replacement?: RbfInfo, replacement?: RbfTree,
connector?: Connector, connector?: Connector,
first?: boolean, first?: boolean,
fullRbf?: boolean,
}
function isTimelineCell(val: RbfTree | TimelineCell): boolean {
return !val || !('tx' in val);
} }
@Component({ @Component({
@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
@Input() txid: string; @Input() txid: string;
rows: TimelineCell[][] = []; rows: TimelineCell[][] = [];
hoverInfo: RbfInfo | void = null; hoverInfo: RbfTree | null = null;
tooltipPosition = null; tooltipPosition = null;
dir: 'rtl' | 'ltr' = 'ltr'; dir: 'rtl' | 'ltr' = 'ltr';
@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
buildTimelines(tree: RbfTree): TimelineCell[][] { buildTimelines(tree: RbfTree): TimelineCell[][] {
if (!tree) return []; if (!tree) return [];
this.flagFullRbf(tree);
const split = this.splitTimelines(tree); const split = this.splitTimelines(tree);
const timelines = this.prepareTimelines(split); const timelines = this.prepareTimelines(split);
return this.connectTimelines(timelines); return this.connectTimelines(timelines);
} }
// sets the fullRbf flag on each transaction in the tree
flagFullRbf(tree: RbfTree): void {
let fullRbf = false;
for (const replaced of tree.replaces) {
if (!replaced.tx.rbf) {
fullRbf = true;
}
replaced.replacedBy = tree.tx;
this.flagFullRbf(replaced);
}
tree.tx.fullRbf = fullRbf;
}
// splits a tree into N leaf-to-root paths // splits a tree into N leaf-to-root paths
splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] { splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] {
const replacements = [...tail, tree]; const replacements = [...tail, tree];
if (tree.replaces.length) { if (tree.replaces.length) {
return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements))); return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
@ -70,7 +89,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
// merges separate leaf-to-root paths into a coherent forking timeline // merges separate leaf-to-root paths into a coherent forking timeline
// represented as a 2D array of Rbf events // represented as a 2D array of Rbf events
prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] { prepareTimelines(lines: RbfTree[][]): (RbfTree | TimelineCell)[][] {
lines.sort((a, b) => b.length - a.length); lines.sort((a, b) => b.length - a.length);
const rows = lines.map(() => []); const rows = lines.map(() => []);
@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
let emptyCount = 0; let emptyCount = 0;
const nextGroups = []; const nextGroups = [];
for (const group of lineGroups) { for (const group of lineGroups) {
const toMerge: { [txid: string]: RbfInfo[][] } = {}; const toMerge: { [txid: string]: RbfTree[][] } = {};
let emptyInGroup = 0; let emptyInGroup = 0;
let first = true; let first = true;
for (const line of group) { for (const line of group) {
@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
} else { } else {
// substitute duplicates with empty cells // substitute duplicates with empty cells
// (we'll fill these in with connecting lines later) // (we'll fill these in with connecting lines later)
rows[index].unshift(null); rows[index].unshift({ connector: true, replacement: head });
} }
// group the tails of the remaining lines for the next iteration // group the tails of the remaining lines for the next iteration
if (line.length) { if (line.length) {
@ -127,7 +146,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
} }
// annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements // annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] { connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] {
const rows: TimelineCell[][] = []; const rows: TimelineCell[][] = [];
timelines.forEach((lines, row) => { timelines.forEach((lines, row) => {
rows.push([]); rows.push([]);
@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
let finished = false; let finished = false;
lines.forEach((replacement, column) => { lines.forEach((replacement, column) => {
const cell: TimelineCell = {}; const cell: TimelineCell = {};
if (replacement) { if (!isTimelineCell(replacement)) {
cell.replacement = replacement; cell.replacement = replacement as RbfTree;
cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf;
} }
rows[row].push(cell); rows[row].push(cell);
if (replacement) { if (!isTimelineCell(replacement)) {
if (!started) { if (!started) {
cell.first = true; cell.first = true;
started = true; started = true;
@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
matched = true; matched = true;
} else if (i === row) { } else if (i === row) {
rows[i][column] = { rows[i][column] = {
connector: 'corner' connector: 'corner',
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
}; };
} else if (nextCell.connector !== 'corner') { } else if (nextCell.connector !== 'corner') {
rows[i][column] = { rows[i][column] = {
connector: 'pipe' connector: 'pipe',
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
}; };
} }
} }

View File

@ -29,11 +29,12 @@ export class RewardStatsComponent implements OnInit {
// Or when we receive a newer block, newer than the latest reward stats api call // Or when we receive a newer block, newer than the latest reward stats api call
this.stateService.blocks$ this.stateService.blocks$
.pipe( .pipe(
switchMap((block) => { switchMap((blocks) => {
if (block[0].height <= this.lastBlockHeight) { const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
if (maxHeight <= this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed return []; // Return an empty stream so the last pipe is not executed
} }
this.lastBlockHeight = block[0].height; this.lastBlockHeight = maxHeight;
return this.apiService.getRewardStats$(); return this.apiService.getRewardStats$();
}) })
) )

View File

@ -2,6 +2,7 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Inpu
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MarkBlockState, StateService } from '../../services/state.service'; import { MarkBlockState, StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants'; import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-start', selector: 'app-start',
@ -55,8 +56,8 @@ export class StartComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount); this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => { this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.blockCount++; this.blockCount = blocks.length;
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8); this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount); this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) { if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
@ -110,9 +111,12 @@ export class StartComponent implements OnInit, OnDestroy {
} }
}); });
this.stateService.blocks$ this.stateService.blocks$
.subscribe((blocks: any) => { .subscribe((blocks: BlockExtended[]) => {
this.countdown = 0; this.countdown = 0;
const block = blocks[0]; const block = blocks[0];
if (!block) {
return;
}
for (const sb in specialBlocks) { for (const sb in specialBlocks) {
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) { if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {

View File

@ -306,7 +306,7 @@
</ng-template> </ng-template>
<ng-template [ngIf]="isLoadingTx && !error"> <ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
<div class="box"> <div class="box">
<div class="row"> <div class="row">
@ -451,7 +451,7 @@
</ng-template> </ng-template>
<ng-template [ngIf]="error"> <ng-template [ngIf]="error && !loadingCachedTx">
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate"> <div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3> <h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>

View File

@ -12,7 +12,7 @@ import {
tap tap
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service'; import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
@ -39,6 +39,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
isLoadingTx = true; isLoadingTx = true;
error: any = undefined; error: any = undefined;
errorUnblinded: any = undefined; errorUnblinded: any = undefined;
loadingCachedTx = false;
waitingForTransaction = false; waitingForTransaction = false;
latestBlock: BlockExtended; latestBlock: BlockExtended;
transactionTime = -1; transactionTime = -1;
@ -49,10 +50,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txReplacedSubscription: Subscription; txReplacedSubscription: Subscription;
txRbfInfoSubscription: Subscription; txRbfInfoSubscription: Subscription;
mempoolPositionSubscription: Subscription; mempoolPositionSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription; queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription; urlFragmentSubscription: Subscription;
mempoolBlocksSubscription: Subscription; mempoolBlocksSubscription: Subscription;
blocksSubscription: Subscription;
fragmentParams: URLSearchParams; fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction; rbfTransaction: undefined | Transaction;
replaced: boolean = false; replaced: boolean = false;
@ -131,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null; this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
}); });
this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.latestBlock = blocks[0];
});
this.fetchCpfpSubscription = this.fetchCpfp$ this.fetchCpfpSubscription = this.fetchCpfp$
.pipe( .pipe(
switchMap((txId) => switchMap((txId) =>
@ -199,6 +204,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCachedTxSubscription = this.fetchCachedTx$ this.fetchCachedTxSubscription = this.fetchCachedTx$
.pipe( .pipe(
tap(() => {
this.loadingCachedTx = true;
}),
switchMap((txId) => switchMap((txId) =>
this.apiService this.apiService
.getRbfCachedTx$(txId) .getRbfCachedTx$(txId)
@ -207,6 +215,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(null); return of(null);
}) })
).subscribe((tx) => { ).subscribe((tx) => {
this.loadingCachedTx = false;
if (!tx) { if (!tx) {
return; return;
} }
@ -338,6 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.feePerVsize = tx.fee / (tx.weight / 4); this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false; this.isLoadingTx = false;
this.error = undefined; this.error = undefined;
this.loadingCachedTx = false;
this.waitingForTransaction = false; this.waitingForTransaction = false;
this.websocketService.startTrackTransaction(tx.txid); this.websocketService.startTrackTransaction(tx.txid);
this.graphExpanded = false; this.graphExpanded = false;
@ -391,9 +401,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
); );
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => { this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
this.latestBlock = block;
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) { if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
this.tx.status = { this.tx.status = {
confirmed: true, confirmed: true,
@ -409,6 +417,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => { this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
if (!this.tx) { if (!this.tx) {
this.error = new Error(); this.error = new Error();
this.loadingCachedTx = false;
this.waitingForTransaction = false; this.waitingForTransaction = false;
} }
this.rbfTransaction = rbfTransaction; this.rbfTransaction = rbfTransaction;
@ -593,13 +602,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCachedTxSubscription.unsubscribe(); this.fetchCachedTxSubscription.unsubscribe();
this.txReplacedSubscription.unsubscribe(); this.txReplacedSubscription.unsubscribe();
this.txRbfInfoSubscription.unsubscribe(); this.txRbfInfoSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe(); this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe(); this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe(); this.mempoolBlocksSubscription.unsubscribe();
this.mempoolPositionSubscription.unsubscribe(); this.mempoolPositionSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe(); this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.leaveTransaction(); this.leaveTransaction();
} }
} }

View File

@ -56,7 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.networkChanged$.subscribe((network) => this.network = network); this.stateService.networkChanged$.subscribe((network) => this.network = network);
if (this.network === 'liquid' || this.network === 'liquidtestnet') { if (this.network === 'liquid' || this.network === 'liquidtestnet') {

View File

@ -132,26 +132,19 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.blocks$ = this.stateService.blocks$ this.blocks$ = this.stateService.blocks$
.pipe( .pipe(
tap(([block]) => { tap((blocks) => {
this.latestBlockHeight = block.height; this.latestBlockHeight = blocks[0].height;
}), }),
scan((acc, [block]) => { switchMap((blocks) => {
if (acc.find((b) => b.height == block.height)) {
return acc;
}
acc.unshift(block);
acc = acc.slice(0, 6);
if (this.stateService.env.MINING_DASHBOARD === true) { if (this.stateService.env.MINING_DASHBOARD === true) {
for (const block of acc) { for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template // @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` + block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
} }
} }
return of(blocks.slice(0, 6));
return acc; })
}, []),
); );
this.transactions$ = this.stateService.transactions$ this.transactions$ = this.stateService.transactions$

View File

@ -8,7 +8,10 @@
</span> </span>
<ng-template #noblockconversion> <ng-template #noblockconversion>
<span [class]="colorClass" *ngIf="(conversions$ | async) as conversions"> <span [class]="colorClass" *ngIf="(conversions$ | async) as conversions; else noconversion">
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }} {{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
</span> </span>
<ng-template #noconversion>
<span>&nbsp;</span>
</ng-template>
</ng-template> </ng-template>

View File

@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo {
mined?: boolean; mined?: boolean;
fullRbf: boolean; fullRbf: boolean;
replaces: RbfTree[]; replaces: RbfTree[];
replacedBy?: RbfTransaction;
} }
export interface DifficultyAdjustment { export interface DifficultyAdjustment {
@ -176,9 +177,10 @@ export interface TransactionStripped {
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }
interface RbfTransaction extends TransactionStripped { export interface RbfTransaction extends TransactionStripped {
rbf?: boolean; rbf?: boolean;
mined?: boolean, mined?: boolean,
fullRbf?: boolean,
} }
export interface MempoolPosition { export interface MempoolPosition {
block: number, block: number,

View File

@ -15,7 +15,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md table-col">
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a> <a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>

View File

@ -1,3 +1,8 @@
.table-col {
max-width: calc(100% - 470px);
overflow: hidden;
}
.table { .table {
font-size: 32px; font-size: 32px;
margin-top: 10px; margin-top: 10px;

View File

@ -1,3 +1,8 @@
.table-col {
max-width: calc(100% - 470px);
overflow: hidden;
}
.table { .table {
margin-top: 6px; margin-top: 6px;
font-size: 32px; font-size: 32px;
@ -18,10 +23,6 @@
} }
} }
.table-col {
max-width: calc(100% - 470px);
}
.map-col { .map-col {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;

View File

@ -18,6 +18,7 @@ export class CacheService {
txCache: { [txid: string]: Transaction } = {}; txCache: { [txid: string]: Transaction } = {};
network: string; network: string;
blockHashCache: { [hash: string]: BlockExtended } = {};
blockCache: { [height: number]: BlockExtended } = {}; blockCache: { [height: number]: BlockExtended } = {};
blockLoading: { [height: number]: boolean } = {}; blockLoading: { [height: number]: boolean } = {};
copiesInBlockQueue: { [height: number]: number } = {}; copiesInBlockQueue: { [height: number]: number } = {};
@ -27,8 +28,10 @@ export class CacheService {
private stateService: StateService, private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
) { ) {
this.stateService.blocks$.subscribe(([block]) => { this.stateService.blocks$.subscribe((blocks) => {
this.addBlockToCache(block); for (const block of blocks) {
this.addBlockToCache(block);
}
this.clearBlocks(); this.clearBlocks();
}); });
this.stateService.chainTip$.subscribe((height) => { this.stateService.chainTip$.subscribe((height) => {
@ -56,8 +59,11 @@ export class CacheService {
} }
addBlockToCache(block: BlockExtended) { addBlockToCache(block: BlockExtended) {
this.blockCache[block.height] = block; if (!this.blockHashCache[block.id]) {
this.bumpBlockPriority(block.height); this.blockHashCache[block.id] = block;
this.blockCache[block.height] = block;
this.bumpBlockPriority(block.height);
}
} }
async loadBlock(height) { async loadBlock(height) {
@ -105,7 +111,9 @@ export class CacheService {
} else if ((this.tip - height) < KEEP_RECENT_BLOCKS) { } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
this.bumpBlockPriority(height); this.bumpBlockPriority(height);
} else { } else {
const block = this.blockCache[height];
delete this.blockCache[height]; delete this.blockCache[height];
delete this.blockHashCache[block.id];
delete this.copiesInBlockQueue[height]; delete this.copiesInBlockQueue[height];
} }
} }
@ -113,6 +121,7 @@ export class CacheService {
// remove all blocks from the cache // remove all blocks from the cache
resetBlockCache() { resetBlockCache() {
this.blockHashCache = {};
this.blockCache = {}; this.blockCache = {};
this.blockLoading = {}; this.blockLoading = {};
this.copiesInBlockQueue = {}; this.copiesInBlockQueue = {};

View File

@ -1,11 +1,11 @@
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 { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } 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 { map, scan, shareReplay, tap } from 'rxjs/operators'; import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
export interface MarkBlockState { export interface MarkBlockState {
@ -90,10 +90,12 @@ export class StateService {
blockVSize: number; blockVSize: number;
env: Env; env: Env;
latestBlockHeight = -1; latestBlockHeight = -1;
blocks: BlockExtended[] = [];
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1); lightningChanged$ = new ReplaySubject<boolean>(1);
blocks$: ReplaySubject<[BlockExtended, string]>; blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
blocks$: Observable<BlockExtended[]>;
transactions$ = new ReplaySubject<TransactionStripped>(6); transactions$ = new ReplaySubject<TransactionStripped>(6);
conversions$ = new ReplaySubject<any>(1); conversions$ = new ReplaySubject<any>(1);
bsqPrice$ = new ReplaySubject<number>(1); bsqPrice$ = new ReplaySubject<number>(1);
@ -102,6 +104,7 @@ export class StateService {
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>(); mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>(); mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
txConfirmed$ = new Subject<[string, BlockExtended]>();
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>(); txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>(); rbfLatest$ = new Subject<RbfTree[]>();
@ -167,8 +170,6 @@ export class StateService {
} }
}); });
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
this.liveMempoolBlockTransactions$ = merge( this.liveMempoolBlockTransactions$ = merge(
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })), this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })), this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
@ -198,8 +199,15 @@ export class StateService {
this.networkChanged$.next(this.env.BASE_MODULE); this.networkChanged$.next(this.env.BASE_MODULE);
} }
this.networkChanged$.subscribe((network) => {
this.transactions$ = new ReplaySubject<TransactionStripped>(6);
this.blocksSubject$.next([]);
});
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4; this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
const savedTimePreference = this.storageService.getValue('time-preference-ltr'); const savedTimePreference = this.storageService.getValue('time-preference-ltr');
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')); const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
// default time direction is right-to-left, unless locale is a RTL language // default time direction is right-to-left, unless locale is a RTL language
@ -336,4 +344,15 @@ export class StateService {
this.chainTip$.next(height); this.chainTip$.next(height);
} }
} }
resetBlocks(blocks: BlockExtended[]): void {
this.blocks = blocks.reverse();
this.blocksSubject$.next(blocks);
}
addBlock(block: BlockExtended): void {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
this.blocksSubject$.next(this.blocks);
}
} }

View File

@ -1,13 +1,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface'; import { WebsocketResponse } from '../interfaces/websocket.interface';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser'; import { TransferState, makeStateKey } from '@angular/platform-browser';
import { BlockExtended } from '../interfaces/node-api.interface'; import { CacheService } from './cache.service';
const OFFLINE_RETRY_AFTER_MS = 2000; const OFFLINE_RETRY_AFTER_MS = 2000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000; const OFFLINE_PING_CHECK_AFTER_MS = 30000;
@ -40,6 +40,7 @@ export class WebsocketService {
private stateService: StateService, private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private transferState: TransferState, private transferState: TransferState,
private cacheService: CacheService,
) { ) {
if (!this.stateService.isBrowser) { if (!this.stateService.isBrowser) {
// @ts-ignore // @ts-ignore
@ -239,13 +240,8 @@ export class WebsocketService {
if (response.blocks && response.blocks.length) { if (response.blocks && response.blocks.length) {
const blocks = response.blocks; const blocks = response.blocks;
let maxHeight = 0; this.stateService.resetBlocks(blocks);
blocks.forEach((block: BlockExtended) => { const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
if (block.height > this.stateService.latestBlockHeight) {
maxHeight = Math.max(maxHeight, block.height);
this.stateService.blocks$.next([block, '']);
}
});
this.stateService.updateChainTip(maxHeight); this.stateService.updateChainTip(maxHeight);
} }
@ -260,7 +256,8 @@ export class WebsocketService {
if (response.block) { if (response.block) {
if (response.block.height === this.stateService.latestBlockHeight + 1) { if (response.block.height === this.stateService.latestBlockHeight + 1) {
this.stateService.updateChainTip(response.block.height); this.stateService.updateChainTip(response.block.height);
this.stateService.blocks$.next([response.block, response.txConfirmed || '']); this.stateService.addBlock(response.block);
this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
} else if (response.block.height > this.stateService.latestBlockHeight + 1) { } else if (response.block.height > this.stateService.latestBlockHeight + 1) {
reinitBlocks = true; reinitBlocks = true;
} }

View File

@ -993,6 +993,10 @@ th {
margin-right: 10px; margin-right: 10px;
} }
} }
.btn-audit {
margin-left: .5em;
}
} }
.scriptmessage { .scriptmessage {

View File

@ -1240,8 +1240,8 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then
FreeBSD) FreeBSD)
echo "[*] Patching Bitcoin Electrs code for FreeBSD" echo "[*] Patching Bitcoin Electrs code for FreeBSD"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak" #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak" #osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
;; ;;
Debian) Debian)
;; ;;

View File

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

View File

@ -16,7 +16,9 @@
"ADVANCED_GBT_MEMPOOL": true, "ADVANCED_GBT_MEMPOOL": true,
"RUST_GBT": true, "RUST_GBT": true,
"USE_SECOND_NODE_FOR_MINFEE": true, "USE_SECOND_NODE_FOR_MINFEE": true,
"DISK_CACHE_BLOCK_INTERVAL": 1 "DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
}, },
"SYSLOG" : { "SYSLOG" : {
"MIN_PRIORITY": "debug" "MIN_PRIORITY": "debug"

View File

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

View File

@ -12,7 +12,9 @@
"ADVANCED_GBT_MEMPOOL": true, "ADVANCED_GBT_MEMPOOL": true,
"RUST_GBT": true, "RUST_GBT": true,
"POLL_RATE_MS": 1000, "POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1 "DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
}, },
"SYSLOG" : { "SYSLOG" : {
"MIN_PRIORITY": "debug" "MIN_PRIORITY": "debug"

View File

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

View File

@ -12,7 +12,9 @@
"ADVANCED_GBT_MEMPOOL": true, "ADVANCED_GBT_MEMPOOL": true,
"RUST_GBT": true, "RUST_GBT": true,
"POLL_RATE_MS": 1000, "POLL_RATE_MS": 1000,
"DISK_CACHE_BLOCK_INTERVAL": 1 "DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
}, },
"SYSLOG" : { "SYSLOG" : {
"MIN_PRIORITY": "debug" "MIN_PRIORITY": "debug"

View File

@ -109,7 +109,10 @@ class Server {
page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false) page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false)
]) ])
if (success === true) { if (success === true) {
const screenshot = await page.screenshot(); const screenshot = await page.screenshot({
captureBeyondViewport: false,
clip: { width: 1200, height: 600, x: 0, y: 0, scale: 1 },
});
return screenshot; return screenshot;
} else if (success === false) { } else if (success === false) {
logger.warn(`failed to render ${path} for ${action} due to client-side error, e.g. requested an invalid txid`); logger.warn(`failed to render ${path} for ${action} due to client-side error, e.g. requested an invalid txid`);