Compare commits
1 Commits
master
...
mononaut/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4922580ea9 |
@ -61,7 +61,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'chain-tips', this.getChainTips.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'stale-tips', this.getStaleTips.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'stale-tips/:height', this.getStaleTips.bind(this))
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs)
|
||||
// Temporarily add txs/package endpoint for all backends until esplora supports it
|
||||
@ -629,18 +628,14 @@ class BitcoinRoutes {
|
||||
private async getStaleTips(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet', 'testnet4', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||
let fromHeight: number | undefined;
|
||||
if (req.params.height !== undefined) {
|
||||
fromHeight = parseInt(req.params.height, 10);
|
||||
if (isNaN(fromHeight) || fromHeight < 0) {
|
||||
handleError(req, res, 400, `Parameter 'height' must be a block height (integer)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
const tips = await chainTips.$getStaleTipsPage(fromHeight, 25);
|
||||
res.json(tips);
|
||||
const tips = await chainTips.getStaleTips();
|
||||
if (tips.length > 0) {
|
||||
res.json(tips);
|
||||
} else {
|
||||
handleError(req, res, 503, `Temporarily unavailable`);
|
||||
return;
|
||||
}
|
||||
} else { // Liquid
|
||||
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
|
||||
return;
|
||||
|
||||
@ -37,7 +37,7 @@ import { calculateGoodBlockCpfp } from './cpfp';
|
||||
import blockProcessor, { BlockProcessingResult, detectTemplateAlgorithm, saveCpfpDataToCpfpSummary } from './block-processor';
|
||||
import mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
import { parseDATUMTemplateCreator, parseDMNDTemplateCreator } from '../utils/bitcoin-script';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
import database from '../database';
|
||||
import { getBlockFirstSeenFromLogs, getOldestLogTimestampFromLogs, scanLogsForBlocksFirstSeen } from '../utils/file-read';
|
||||
|
||||
@ -359,8 +359,6 @@ class Blocks {
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
} else if (extras.pool.name === 'DMND') {
|
||||
extras.pool.minerNames = parseDMNDTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { BlockExtended } from '../mempool.interfaces';
|
||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||
import BlocksRepository from '../repositories/BlocksRepository';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
@ -24,29 +23,26 @@ export interface StaleTip extends ChainTip {
|
||||
export interface OrphanedBlock {
|
||||
height: number;
|
||||
hash: string;
|
||||
branchlen: number;
|
||||
status: 'valid-fork' | 'valid-headers' | 'headers-only';
|
||||
prevhash: string;
|
||||
}
|
||||
|
||||
class ChainTips {
|
||||
private chainTips: ChainTip[] = [];
|
||||
private validChainTips: ChainTip[] = []; // 'valid-fork' and 'valid-headers' only, in descending height order
|
||||
private staleBlocks: Record<string, BlockExtended> = {};
|
||||
private staleTips: Record<number, StaleTip> = {};
|
||||
private orphanedBlocks: { [hash: string]: OrphanedBlock } = {};
|
||||
private blockCache: { [hash: string]: OrphanedBlock } = {};
|
||||
private orphansByHeight: { [height: number]: OrphanedBlock[] } = {};
|
||||
private indexingOrphanedBlocks = false;
|
||||
private indexingQueue: { blockhash?: string, block?: IEsploraApi.Block, tip: OrphanedBlock }[] = [];
|
||||
|
||||
private staleBlocksCacheSize = 50;
|
||||
private staleTipsCacheSize = 50;
|
||||
private maxIndexingQueueSize = 100;
|
||||
|
||||
/** @asyncSafe */
|
||||
public async updateOrphanedBlocks(): Promise<void> {
|
||||
try {
|
||||
this.chainTips = await bitcoinClient.getChainTips();
|
||||
this.validChainTips = this.chainTips.filter(tip => tip.status === 'valid-fork' || tip.status === 'valid-headers').sort((a, b) => b.height - a.height);
|
||||
|
||||
const activeTipHeight = this.chainTips.find(tip => tip.status === 'active')?.height || (await bitcoinApi.$getBlockHeightTip());
|
||||
let minIndexHeight = 0;
|
||||
@ -73,7 +69,6 @@ class ChainTips {
|
||||
orphan = {
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
branchlen: chain.branchlen,
|
||||
status: chain.status,
|
||||
prevhash: block.previousblockhash,
|
||||
};
|
||||
@ -124,7 +119,13 @@ class ChainTips {
|
||||
this.orphansByHeight[orphan.height].push(orphan);
|
||||
}
|
||||
|
||||
this.trimStaleBlocksCache();
|
||||
const heightsToKeep = new Set(this.chainTips.filter(tip => tip.status !== 'active').map(tip => tip.height));
|
||||
const heightsToRemove: number[] = Object.keys(this.staleTips).map(Number).filter(height => !heightsToKeep.has(height));
|
||||
for (const height of heightsToRemove) {
|
||||
delete this.staleTips[height];
|
||||
}
|
||||
|
||||
this.trimStaleTipsCache();
|
||||
|
||||
// index new orphaned blocks in the background
|
||||
void this.$indexOrphanedBlocks();
|
||||
@ -156,7 +157,7 @@ class ChainTips {
|
||||
}
|
||||
let staleBlock: BlockExtended | undefined;
|
||||
const alreadyIndexed = await BlocksSummariesRepository.$isSummaryIndexed(block.id);
|
||||
const needToCache = this.shouldCacheStaleBlock(block.id, block.height);
|
||||
const needToCache = Object.keys(this.staleTips).length < this.staleTipsCacheSize || block.height > Object.keys(this.staleTips).map(Number).sort((a, b) => b - a)[this.staleTipsCacheSize - 1];
|
||||
if (!alreadyIndexed) {
|
||||
staleBlock = await blocks.$indexBlock(block.id, block, true);
|
||||
await blocks.$indexBlockSummary(block.id, block.height, true);
|
||||
@ -167,9 +168,16 @@ class ChainTips {
|
||||
}
|
||||
|
||||
if (staleBlock && needToCache) {
|
||||
// ensure the canonical block is correctly indexed
|
||||
await blocks.$indexBlockByHeight(staleBlock.height);
|
||||
this.cacheStaleBlock(staleBlock);
|
||||
const canonicalBlock = await blocks.$indexBlockByHeight(staleBlock.height);
|
||||
this.staleTips[staleBlock.height] = {
|
||||
height: staleBlock.height,
|
||||
hash: staleBlock.id,
|
||||
branchlen: tip.height - staleBlock.height,
|
||||
status: tip.status,
|
||||
stale: staleBlock,
|
||||
canonical: canonicalBlock,
|
||||
};
|
||||
this.trimStaleTipsCache();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Failed to index orphaned block ${block?.id} at height ${block?.height}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
@ -178,42 +186,12 @@ class ChainTips {
|
||||
this.indexingOrphanedBlocks = false;
|
||||
}
|
||||
|
||||
private shouldCacheStaleBlock(hash: string, height: number): boolean {
|
||||
// already cached
|
||||
if (this.staleBlocks[hash]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// cache is not full
|
||||
const cachedBlocks = Object.values(this.staleBlocks);
|
||||
if (cachedBlocks.length < this.staleBlocksCacheSize) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise cache if this block is newer than the oldest in the cache
|
||||
const oldestCachedHeight = cachedBlocks.reduce((min, block) => Math.min(min, block.height), Infinity);
|
||||
return height >= oldestCachedHeight;
|
||||
}
|
||||
|
||||
private cacheStaleBlock(block: BlockExtended): void {
|
||||
this.staleBlocks[block.id] = block;
|
||||
this.trimStaleBlocksCache();
|
||||
}
|
||||
|
||||
// evict the oldest stale blocks until the cache is within the size limit
|
||||
private trimStaleBlocksCache(): void {
|
||||
// sort by height
|
||||
const cachedBlocks = Object.values(this.staleBlocks).sort((a, b) => {
|
||||
if (b.height !== a.height) {
|
||||
return b.height - a.height;
|
||||
}
|
||||
// tie-break by hash
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
// delete everything beyond the size limit
|
||||
if (cachedBlocks.length > this.staleBlocksCacheSize) {
|
||||
for (const block of cachedBlocks.slice(this.staleBlocksCacheSize)) {
|
||||
delete this.staleBlocks[block.id];
|
||||
private trimStaleTipsCache(): void {
|
||||
const staleTipHeights = Object.keys(this.staleTips).map(Number).sort((a, b) => b - a);
|
||||
if (staleTipHeights.length > this.staleTipsCacheSize) {
|
||||
const heightsToDiscard = staleTipHeights.slice(this.staleTipsCacheSize);
|
||||
for (const height of heightsToDiscard) {
|
||||
delete this.staleTips[height];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,51 +208,8 @@ class ChainTips {
|
||||
return this.chainTips;
|
||||
}
|
||||
|
||||
/**
|
||||
* get paginated stale chain tips
|
||||
* @param fromHeight - start height (exclusive)
|
||||
* @param count - requested page size (target, but not strictly enforced)
|
||||
*
|
||||
* @asyncSafe
|
||||
*/
|
||||
public async $getStaleTipsPage(fromHeight: number | undefined, count: number): Promise<StaleTip[]> {
|
||||
const start = fromHeight === undefined ? 0 : this.validChainTips.findIndex(tip => tip.height < fromHeight);
|
||||
// no tips beyond the requested height, we can return early
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// fill the response array with hydrated tip data
|
||||
const tips: StaleTip[] = [];
|
||||
let lastHeight;
|
||||
for (let index = start; index < this.validChainTips.length; index++) {
|
||||
const staleTip = this.validChainTips[index];
|
||||
// stretch the page to include any remaining blocks at the last included height to avoid pagination gaps with a height-based cursor
|
||||
if (tips.length >= count) {
|
||||
if (staleTip.height !== lastHeight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// fetch blocks from caches if available, or DB otherwise
|
||||
const canonical = blocks.getBlocks().find(block => block.height === staleTip.height) || await BlocksRepository.$getBlockByHeight(staleTip.height);
|
||||
let stale: BlockExtended | null | undefined = this.staleBlocks[staleTip.hash];
|
||||
if (!stale) {
|
||||
stale = await BlocksRepository.$getBlockByHash(staleTip.hash);
|
||||
}
|
||||
// skip tips with missing block data
|
||||
if (!canonical || !stale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tips.push({
|
||||
...staleTip,
|
||||
stale,
|
||||
canonical,
|
||||
});
|
||||
lastHeight = staleTip.height;
|
||||
}
|
||||
|
||||
return tips;
|
||||
public getStaleTips(): StaleTip[] {
|
||||
return Object.values(this.staleTips).sort((a, b) => b.height - a.height);
|
||||
}
|
||||
|
||||
clearOrphanCacheAboveHeight(height: number): void {
|
||||
@ -299,4 +234,4 @@ class ChainTips {
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChainTips();
|
||||
export default new ChainTips();
|
||||
@ -749,15 +749,7 @@ export class Common {
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
const tokenRelated = (flags & (TransactionFlags.inscription | TransactionFlags.op_return)) !== 0n;
|
||||
if (!addressReuse &&
|
||||
tx.vin.length >= 5 &&
|
||||
tx.vout.length >= 5 &&
|
||||
(Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 &&
|
||||
!tokenRelated &&
|
||||
tx.vin.length / tx.vout.length < 5 &&
|
||||
tx.vin.length / tx.vout.length > 0.2
|
||||
) {
|
||||
if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) {
|
||||
flags |= TransactionFlags.coinjoin;
|
||||
}
|
||||
// more than 5:1 input:output ratio
|
||||
|
||||
@ -14,7 +14,7 @@ import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { parseDATUMTemplateCreator, parseDMNDTemplateCreator } from '../utils/bitcoin-script';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
import poolsUpdater from '../tasks/pools-updater';
|
||||
|
||||
interface DatabaseBlock {
|
||||
@ -575,34 +575,8 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonical block hash at a given height
|
||||
* @asyncSafe
|
||||
*/
|
||||
public async $getCanonicalBlockHashByHeight(height: number): Promise<string | null> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`
|
||||
SELECT hash
|
||||
FROM blocks
|
||||
WHERE height = ? AND stale = 0
|
||||
LIMIT 1`,
|
||||
[height]
|
||||
);
|
||||
|
||||
if (rows.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rows[0].hash;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get canonical block hash at height ${height}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one block by hash
|
||||
* @asyncSafe
|
||||
*/
|
||||
public async $getBlockByHash(hash: string): Promise<BlockExtended | null> {
|
||||
try {
|
||||
@ -1282,10 +1256,6 @@ class BlocksRepository {
|
||||
blk.previousblockhash = dbBlk.previousblockhash;
|
||||
blk.mediantime = dbBlk.mediantime;
|
||||
blk.indexVersion = dbBlk.index_version;
|
||||
blk.stale = dbBlk.stale;
|
||||
if (dbBlk.stale) {
|
||||
blk.canonical = await this.$getCanonicalBlockHashByHeight(dbBlk.height) || undefined;
|
||||
}
|
||||
// BlockExtension
|
||||
extras.totalFees = dbBlk.totalFees;
|
||||
extras.medianFee = dbBlk.medianFee;
|
||||
@ -1374,8 +1344,6 @@ class BlocksRepository {
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
} else if (extras.pool.name === 'DMND') {
|
||||
extras.pool.minerNames = parseDMNDTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
|
||||
blk.extras = <BlockExtension>extras;
|
||||
|
||||
@ -224,27 +224,4 @@ export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null
|
||||
tagString = tagString.replace('\x00', '');
|
||||
|
||||
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
|
||||
}
|
||||
|
||||
/** Extracts miner names from a DMND coinbase transaction */
|
||||
export function parseDMNDTemplateCreator(coinbaseRaw: string): string[] {
|
||||
try {
|
||||
if (!coinbaseRaw || coinbaseRaw.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(coinbaseRaw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(coinbaseRaw, 'hex');
|
||||
const blockHeightLength = bytes[0];
|
||||
const tagDelimiterIndex = 1 + blockHeightLength;
|
||||
if (bytes.length <= tagDelimiterIndex || bytes[tagDelimiterIndex] !== 0x00) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tagStart = tagDelimiterIndex + 1;
|
||||
const tagEnd = bytes.indexOf(0x00, tagStart);
|
||||
const tags = bytes.subarray(tagStart, tagEnd === -1 ? undefined : tagEnd);
|
||||
return tags.toString('utf8').split('/').slice(1, -1);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
79
frontend/package-lock.json
generated
79
frontend/package-lock.json
generated
@ -27,7 +27,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "~6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.7.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
|
||||
"@noble/secp256k1": "^3.1.0",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
"bootstrap": "~5.3.8",
|
||||
"clipboard": "^2.0.11",
|
||||
@ -6464,9 +6464,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/secp256k1": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
|
||||
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
|
||||
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
@ -12367,9 +12368,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.25",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
|
||||
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
|
||||
"version": "4.12.16",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
|
||||
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@ -13116,19 +13118,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@ -13283,12 +13276,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
||||
"integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
"shell-quote": "^1.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy-ass": {
|
||||
@ -17138,9 +17131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.16",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
|
||||
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
@ -22109,9 +22102,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@noble/secp256k1": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
|
||||
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g=="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
|
||||
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
@ -26079,9 +26072,9 @@
|
||||
}
|
||||
},
|
||||
"hono": {
|
||||
"version": "4.12.25",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
|
||||
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="
|
||||
"version": "4.12.16",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
|
||||
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "9.0.2",
|
||||
@ -26585,9 +26578,9 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
@ -26709,12 +26702,12 @@
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"launch-editor": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
||||
"integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
|
||||
"requires": {
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
"shell-quote": "^1.8.3"
|
||||
}
|
||||
},
|
||||
"lazy-ass": {
|
||||
@ -29376,9 +29369,9 @@
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="
|
||||
},
|
||||
"tar": {
|
||||
"version": "7.5.16",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
|
||||
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"requires": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "~6.7.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
"@noble/secp256k1": "^3.1.0",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"bootstrap": "~5.3.8",
|
||||
"clipboard": "^2.0.11",
|
||||
"domino": "^2.1.6",
|
||||
|
||||
@ -53,23 +53,15 @@
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<ng-container *ngIf="block.extras.pool.minerSlug && block.extras.pool.minerName; else minerName">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.minerSlug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.minerName + ' miner'">
|
||||
<span class="miner-name">{{ block.extras.pool.minerName }}</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</ng-template>
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||
|
||||
@ -188,21 +188,14 @@
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<ng-container *ngIf="block.extras.pool.minerSlug && block.extras.pool.minerName; else minerName">
|
||||
<app-mining-pool [slug]="block.extras.pool.minerSlug" [name]="block.extras.pool.minerName" [showName]="false"></app-mining-pool>
|
||||
<span class="miner-name">{{ block.extras.pool.minerName }}</span>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.name"></app-mining-pool>
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.name"></app-mining-pool>
|
||||
</ng-template>
|
||||
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
|
||||
@if (block.extras.pool.minerNames[1].length > 16) {
|
||||
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.name"></app-mining-pool>
|
||||
</a>
|
||||
</td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||
|
||||
@ -79,14 +79,8 @@
|
||||
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [class.miner-name]="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<ng-container *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''; else centralisedPool">
|
||||
<ng-container *ngIf="block.extras.pool.minerSlug && block.extras.pool.minerName; else minerName">
|
||||
<app-mining-pool [slug]="block.extras.pool.minerSlug" [name]="block.extras.pool.minerName" [showName]="false"></app-mining-pool>
|
||||
{{ block.extras.pool.minerName }}
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.minerNames[1]" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
</ng-template>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.minerNames[1]" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ block.extras.pool.minerNames[1] }}
|
||||
</ng-container>
|
||||
<ng-template #centralisedPool>
|
||||
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.name"></app-mining-pool>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="chain-tips" infiniteScroll [alwaysCallback]="true" [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" (scrolled)="loadMore()" [ngStyle]="{ 'min-height': '295px', 'opacity': isLoading ? '0.75' : '1' }">
|
||||
<div class="chain-tips" [ngStyle]="{ 'min-height': '295px', 'opacity': isLoading ? '0.75' : '1' }">
|
||||
<ng-container *ngIf="chainTips$ | async as chainTips">
|
||||
<div *ngFor="let chainTip of chainTips" class="chain-tip">
|
||||
<p class="info">
|
||||
@ -17,7 +17,7 @@
|
||||
<span *ngSwitchCase="'invalid'" class="badge bg-info" i18n="chain-tips.invalid">Invalid</span>
|
||||
<span *ngSwitchDefault>{{ chainTip.status }}</span>
|
||||
</span>
|
||||
<span class="badge bg-secondary depth-badge" i18n="chain-tips.depth">Depth {{ chainTip.branchlen }}</span>
|
||||
<span class="badge bg-secondary depth-badge" i18n="chain-tips.depth">Depth {{ chainTip.branchlen + 1 }}</span>
|
||||
</span>
|
||||
</p>
|
||||
<div class="stale-tip-wrapper">
|
||||
@ -57,14 +57,8 @@
|
||||
<div class="animated" *ngIf="chainTip.stale?.extras?.pool != undefined">
|
||||
<a class="badge" [class.miner-name]="chainTip.stale.extras.pool.minerNames?.length > 1 && chainTip.stale.extras.pool.minerNames[1] != ''" [routerLink]="[('/mining/pool/' + chainTip.stale.extras.pool.slug) | relativeUrl]">
|
||||
<ng-container *ngIf="chainTip.stale.extras.pool.minerNames?.length > 1 && chainTip.stale.extras.pool.minerNames[1] != ''; else staleBlockCentralisedPool">
|
||||
<ng-container *ngIf="chainTip.stale.extras.pool.minerSlug && chainTip.stale.extras.pool.minerName; else minerName">
|
||||
<app-mining-pool [slug]="chainTip.stale.extras.pool.minerSlug" [name]="chainTip.stale.extras.pool.minerName" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.stale.extras.pool.minerName }}
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<app-mining-pool [slug]="chainTip.stale.extras.pool.slug" [name]="chainTip.stale.extras.pool.name" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.stale.extras.pool.minerNames[1] }}
|
||||
</ng-template>
|
||||
<app-mining-pool [slug]="chainTip.stale.extras.pool.slug" [name]="chainTip.stale.extras.pool.name" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.stale.extras.pool.minerNames[1] }}
|
||||
</ng-container>
|
||||
<ng-template #staleBlockCentralisedPool>
|
||||
<app-mining-pool [slug]="chainTip.stale.extras.pool.slug" [name]="chainTip.stale.extras.pool.name"></app-mining-pool>
|
||||
@ -109,14 +103,8 @@
|
||||
<div class="animated" *ngIf="chainTip.canonical?.extras?.pool != undefined">
|
||||
<a class="badge" [class.miner-name]="chainTip.canonical.extras.pool.minerNames?.length > 1 && chainTip.canonical.extras.pool.minerNames[1] != ''" [routerLink]="[('/mining/pool/' + chainTip.canonical.extras.pool.slug) | relativeUrl]">
|
||||
<ng-container *ngIf="chainTip.canonical.extras.pool.minerNames?.length > 1 && chainTip.canonical.extras.pool.minerNames[1] != ''; else canonicalBlockCentralisedPool">
|
||||
<ng-container *ngIf="chainTip.canonical.extras.pool.minerSlug && chainTip.canonical.extras.pool.minerName; else minerName">
|
||||
<app-mining-pool [slug]="chainTip.canonical.extras.pool.minerSlug" [name]="chainTip.canonical.extras.pool.minerName" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.canonical.extras.pool.minerName }}
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<app-mining-pool [slug]="chainTip.canonical.extras.pool.slug" [name]="chainTip.canonical.extras.pool.name" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.canonical.extras.pool.minerNames[1] }}
|
||||
</ng-template>
|
||||
<app-mining-pool [slug]="chainTip.canonical.extras.pool.slug" [name]="chainTip.canonical.extras.pool.name" [logoStyle]="'filter: grayscale(100%) brightness(1.5);'" [showName]="false"></app-mining-pool>
|
||||
{{ chainTip.canonical.extras.pool.minerNames[1] }}
|
||||
</ng-container>
|
||||
<ng-template #canonicalBlockCentralisedPool>
|
||||
<app-mining-pool [slug]="chainTip.canonical.extras.pool.slug" [name]="chainTip.canonical.extras.pool.name"></app-mining-pool>
|
||||
@ -128,19 +116,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-chain-tips" *ngIf="!chainTips?.length && !isLoading && !error">
|
||||
<div class="no-chain-tips" *ngIf="!chainTips?.length">
|
||||
<p i18n="chain-tips.no-stale-blocks-yet">This node hasn't seen any stale blocks yet!</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="isLoadingMore" class="text-center loading-more">
|
||||
<div class="spinner-border" role="status"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && !isLoading && !isLoadingMore" class="alert alert-danger text-center loading-more" role="alert">
|
||||
<span i18n="chain-tips.load-error">Failed to load more stale chain tips.</span>
|
||||
<button type="button" class="btn btn-sm btn-danger ms-2" (click)="retryLoadMore()" i18n="chain-tips.retry">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, timer } from 'rxjs';
|
||||
import { catchError, map, retry, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { StaleTip, BlockExtended } from '@interfaces/node-api.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
@ -16,12 +16,9 @@ import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
})
|
||||
export class StaleList implements OnInit {
|
||||
chainTips$: Observable<StaleTip[]>;
|
||||
loadMoreSubject = new BehaviorSubject<number | undefined>(undefined);
|
||||
chainTips: StaleTip[] = [];
|
||||
nextChainTipSubject = new BehaviorSubject(null);
|
||||
urlFragmentSubscription: Subscription;
|
||||
isLoading = true;
|
||||
isLoadingMore = false;
|
||||
fullyLoaded = false;
|
||||
error: unknown = null;
|
||||
|
||||
gradientColors = {
|
||||
'': ['var(--mainnet-alt)', 'var(--primary)'],
|
||||
@ -40,73 +37,32 @@ export class StaleList implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.chainTips$ = this.loadMoreSubject.pipe(
|
||||
switchMap((height) => this.apiService.getStaleTips$(height).pipe(
|
||||
map((chainTips) => chainTips.filter((chainTip) => chainTip.status !== 'active') as StaleTip[]),
|
||||
retry({
|
||||
count: 2,
|
||||
delay: (err) => {
|
||||
this.error = err;
|
||||
return timer(1000);
|
||||
},
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.isLoading = false;
|
||||
this.isLoadingMore = false;
|
||||
return of(null);
|
||||
}),
|
||||
)),
|
||||
tap((newChainTips) => {
|
||||
if (newChainTips === null) {
|
||||
return;
|
||||
}
|
||||
this.error = null;
|
||||
if (!newChainTips.length) {
|
||||
this.fullyLoaded = true;
|
||||
} else {
|
||||
newChainTips.forEach((chainTip) => {
|
||||
if (chainTip.stale?.extras) {
|
||||
chainTip.stale.extras.minFee = this.getMinBlockFee(chainTip.stale);
|
||||
chainTip.stale.extras.maxFee = this.getMaxBlockFee(chainTip.stale);
|
||||
}
|
||||
if (chainTip.canonical?.extras) {
|
||||
chainTip.canonical.extras.minFee = this.getMinBlockFee(chainTip.canonical);
|
||||
chainTip.canonical.extras.maxFee = this.getMaxBlockFee(chainTip.canonical);
|
||||
}
|
||||
});
|
||||
this.chainTips = this.chainTips.concat(newChainTips);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.isLoadingMore = false;
|
||||
this.chainTips$ = this.apiService.getStaleTips$().pipe(
|
||||
map((chainTips) => {
|
||||
const filtered = chainTips.filter((chainTip) => chainTip.status !== 'active') as StaleTip[];
|
||||
|
||||
filtered.forEach((chainTip) => {
|
||||
if (chainTip.stale?.extras) {
|
||||
chainTip.stale.extras.minFee = this.getMinBlockFee(chainTip.stale);
|
||||
chainTip.stale.extras.maxFee = this.getMaxBlockFee(chainTip.stale);
|
||||
}
|
||||
if (chainTip.canonical?.extras) {
|
||||
chainTip.canonical.extras.minFee = this.getMinBlockFee(chainTip.canonical);
|
||||
chainTip.canonical.extras.maxFee = this.getMaxBlockFee(chainTip.canonical);
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}),
|
||||
map(() => this.chainTips),
|
||||
share(),
|
||||
tap(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
);
|
||||
|
||||
this.seoService.setTitle($localize`:@@page.stale-chain-tips:Stale Chain Tips`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.stale-chain-tips:See the most recent stale chain tips on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network.`);
|
||||
}
|
||||
|
||||
loadMore(): void {
|
||||
if (this.isLoading || this.isLoadingMore || this.fullyLoaded || this.error) {
|
||||
return;
|
||||
}
|
||||
this.isLoadingMore = true;
|
||||
const height = this.chainTips[this.chainTips.length - 1]?.height;
|
||||
this.loadMoreSubject.next(height);
|
||||
}
|
||||
|
||||
retryLoadMore(): void {
|
||||
if (this.isLoading || this.isLoadingMore || this.fullyLoaded) {
|
||||
return;
|
||||
}
|
||||
this.error = null;
|
||||
this.isLoadingMore = true;
|
||||
const height = this.chainTips[this.chainTips.length - 1]?.height;
|
||||
this.loadMoreSubject.next(height);
|
||||
}
|
||||
|
||||
getBlockGradient(block: BlockExtended): string {
|
||||
if (!block || !block.weight) {
|
||||
return 'var(--secondary)';
|
||||
|
||||
@ -333,21 +333,14 @@
|
||||
@if (pool) {
|
||||
<td class="wrap-cell">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: var(--fg);padding:0;">
|
||||
<ng-container *ngIf="pool.minerSlug && pool.minerName; else minerName">
|
||||
<app-mining-pool [slug]="pool.minerSlug" [name]="pool.minerName" [showName]="false"></app-mining-pool>
|
||||
<span class="miner-name">{{ pool.minerName }}</span>
|
||||
<app-mining-pool [slug]="pool.slug" [name]="pool.name"></app-mining-pool>
|
||||
</ng-container>
|
||||
<ng-template #minerName>
|
||||
<span class="miner-name" *ngIf="pool.minerNames?.length > 1 && pool.minerNames[1] != ''">
|
||||
@if (pool.minerNames[1].length > 16) {
|
||||
{{ pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<app-mining-pool [slug]="pool.slug" [name]="pool.name"></app-mining-pool>
|
||||
</ng-template>
|
||||
<span class="miner-name" *ngIf="pool.minerNames?.length > 1 && pool.minerNames[1] != ''">
|
||||
@if (pool.minerNames[1].length > 16) {
|
||||
{{ pool.minerNames[1].slice(0, 15) }}…
|
||||
} @else {
|
||||
{{ pool.minerNames[1] }}
|
||||
}
|
||||
</span>
|
||||
<app-mining-pool [slug]="pool.slug" [name]="pool.name"></app-mining-pool>
|
||||
</a>
|
||||
</td>
|
||||
} @else {
|
||||
|
||||
@ -45,8 +45,6 @@ export interface Pool {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
minerName?: string;
|
||||
minerSlug?: string;
|
||||
minerNames: string[] | null;
|
||||
}
|
||||
|
||||
|
||||
@ -330,7 +330,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="why-block-timestamps-dont-always-increase">
|
||||
<p>Block validation rules do not strictly require that a block's timestamp be more recent than the timestamp of the block preceding it. Without a central authority, it's impossible to know what the exact correct time is. Instead, the Bitcoin protocol requires that a block's timestamp meet certain requirements. One of those requirements is that <a href="https://en.bitcoin.it/wiki/Block_timestamp" target="_blank">a block's timestamp cannot be older than the median timestamp of the 12 blocks that came before it</a>.</p><p>As a result, timestamps are only accurate to within an hour or so, which sometimes results in blocks with timestamps that appear out of order.</p>
|
||||
<p>Block validation rules do not strictly require that a block's timestamp be more recent than the timestamp of the block preceding it. Without a central authority, it's impossible to know what the exact correct time is. Instead, the Bitcoin protocol requires that a block's timestamp meet certain requirements. One of those requirements is that a block's timestamp cannot be older than the median timestamp of the 12 blocks that came before it. See more details <a href="https://en.bitcoin.it/wiki/Block_timestamp" target="_blank">here</a>.</p><p>As a result, timestamps are only accurate to within an hour or so, which sometimes results in blocks with timestamps that appear out of order.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="why-dont-fee-ranges-match">
|
||||
|
||||
@ -224,8 +224,6 @@ export interface BlockExtension {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
minerName?: string;
|
||||
minerSlug?: string;
|
||||
minerNames: string[] | null;
|
||||
}
|
||||
orphans?: {
|
||||
|
||||
@ -2,14 +2,13 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights,
|
||||
RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult, WalletAddress, Treasury, SubmitPackageResult, ChainTip, StaleTip } from '@interfaces/node-api.interface';
|
||||
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, switchMap, take, tap } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { Conversion } from '@app/services/price.service';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { WebsocketResponse } from '@interfaces/websocket.interface';
|
||||
import { TxAuditStatus } from '@components/transaction/transaction.component';
|
||||
import { MinersService } from '@app/services/miners.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -25,8 +24,7 @@ export class ApiService {
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
private minersService: MinersService,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
this.apiBaseUrl = ''; // use relative URL by default
|
||||
if (!stateService.isBrowser) { // except when inside AU SSR process
|
||||
@ -174,10 +172,8 @@ export class ApiService {
|
||||
return this.httpClient.get<ChainTip[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/chain-tips');
|
||||
}
|
||||
|
||||
getStaleTips$(height?: number): Observable<StaleTip[]> {
|
||||
return this.httpClient.get<StaleTip[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/stale-tips' + (height !== undefined ? `/${height}` : ``)).pipe(
|
||||
switchMap((staleTips) => this.minersService.applyStaleTipsMinerDetails$(staleTips))
|
||||
);
|
||||
getStaleTips$(): Observable<StaleTip[]> {
|
||||
return this.httpClient.get<StaleTip[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/stale-tips');
|
||||
}
|
||||
|
||||
liquidPegs$(): Observable<CurrentPegs> {
|
||||
@ -312,8 +308,6 @@ export class ApiService {
|
||||
return this.httpClient.get<BlockExtended[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}/blocks` +
|
||||
(fromHeight !== undefined ? `/${fromHeight}` : '')
|
||||
).pipe(
|
||||
switchMap((blocks) => this.minersService.applyBlocksMinerDetails$(blocks))
|
||||
);
|
||||
}
|
||||
|
||||
@ -321,15 +315,11 @@ export class ApiService {
|
||||
return this.httpClient.get<BlockExtended[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/blocks` +
|
||||
(from !== undefined ? `/${from}` : ``)
|
||||
).pipe(
|
||||
switchMap((blocks) => this.minersService.applyBlocksMinerDetails$(blocks))
|
||||
);
|
||||
}
|
||||
|
||||
getBlock$(hash: string): Observable<BlockExtended> {
|
||||
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash).pipe(
|
||||
switchMap((block) => this.minersService.applyBlockMinerDetails$(block))
|
||||
);
|
||||
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash);
|
||||
}
|
||||
|
||||
getBlockDataFromTimestamp$(timestamp: number): Observable<any> {
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map, shareReplay } from 'rxjs/operators';
|
||||
import { BlockExtended, StaleTip } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
|
||||
interface Miner {
|
||||
name: string;
|
||||
slug: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MinersService {
|
||||
private apiBaseUrl = '';
|
||||
private miners$: Observable<Miner[]>;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
if (!stateService.isBrowser) {
|
||||
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
|
||||
}
|
||||
this.miners$ = this.httpClient.get<Miner[]>(this.apiBaseUrl + '/resources/miners.json').pipe(
|
||||
catchError(() => of([])),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public applyBlockMinerDetails$(block: BlockExtended): Observable<BlockExtended> {
|
||||
if (!block?.extras?.pool?.minerNames?.length) {
|
||||
return of(block);
|
||||
}
|
||||
return this.miners$.pipe(map((miners) => this.applyBlockMinerDetails(block, miners)));
|
||||
}
|
||||
|
||||
public applyBlocksMinerDetails$(blocks: BlockExtended[]): Observable<BlockExtended[]> {
|
||||
if (!blocks?.some((block) => block?.extras?.pool?.minerNames?.length)) {
|
||||
return of(blocks);
|
||||
}
|
||||
return this.miners$.pipe(map((miners) => blocks.map((block) => this.applyBlockMinerDetails(block, miners))));
|
||||
}
|
||||
|
||||
public applyStaleTipsMinerDetails$(staleTips: StaleTip[]): Observable<StaleTip[]> {
|
||||
if (!staleTips?.some((staleTip) => staleTip.stale?.extras?.pool?.minerNames?.length || staleTip.canonical?.extras?.pool?.minerNames?.length)) {
|
||||
return of(staleTips);
|
||||
}
|
||||
return this.miners$.pipe(
|
||||
map((miners) => staleTips.map((staleTip) => {
|
||||
this.applyBlockMinerDetails(staleTip.stale, miners);
|
||||
this.applyBlockMinerDetails(staleTip.canonical, miners);
|
||||
return staleTip;
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private applyBlockMinerDetails(block: BlockExtended, miners: Miner[]): BlockExtended {
|
||||
const minerNames = block?.extras?.pool?.minerNames;
|
||||
if (!minerNames?.length) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const miner = miners.find((minerEntry) => minerEntry.tags.some((tag) => minerNames.includes(tag)));
|
||||
if (miner?.name && miner.slug) {
|
||||
block.extras.pool.minerName = miner.name;
|
||||
block.extras.pool.minerSlug = miner.slug;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@ -3,13 +3,12 @@ import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { WebsocketResponse } from '@interfaces/websocket.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { firstValueFrom, of, Subscription } from 'rxjs';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { catchError, take } from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TransferState, makeStateKey } from '@angular/core';
|
||||
import { CacheService } from '@app/services/cache.service';
|
||||
import { uncompressDeltaChange, uncompressTx } from '@app/shared/common.utils';
|
||||
import { MinersService } from '@app/services/miners.service';
|
||||
|
||||
const OFFLINE_RETRY_AFTER_MS = 2000;
|
||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
|
||||
@ -52,7 +51,6 @@ export class WebsocketService {
|
||||
private apiService: ApiService,
|
||||
private transferState: TransferState,
|
||||
private cacheService: CacheService,
|
||||
private minersService: MinersService,
|
||||
) {
|
||||
if (!this.stateService.isBrowser) {
|
||||
// @ts-ignore
|
||||
@ -358,14 +356,9 @@ export class WebsocketService {
|
||||
|
||||
if (response.blocks && response.blocks.length) {
|
||||
const blocks = response.blocks;
|
||||
this.minersService.applyBlocksMinerDetails$(blocks).pipe(
|
||||
take(1),
|
||||
catchError(() => of(blocks))
|
||||
).subscribe((mappedBlocks) => {
|
||||
this.stateService.resetBlocks(mappedBlocks);
|
||||
const maxHeight = mappedBlocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
|
||||
this.stateService.updateChainTip(maxHeight);
|
||||
});
|
||||
this.stateService.resetBlocks(blocks);
|
||||
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
|
||||
this.stateService.updateChainTip(maxHeight);
|
||||
}
|
||||
|
||||
if (response.tx) {
|
||||
@ -378,14 +371,9 @@ export class WebsocketService {
|
||||
|
||||
if (response.block) {
|
||||
if (response.block.height === this.stateService.latestBlockHeight + 1) {
|
||||
this.minersService.applyBlockMinerDetails$(response.block).pipe(
|
||||
take(1),
|
||||
catchError(() => of(response.block))
|
||||
).subscribe((block) => {
|
||||
this.stateService.updateChainTip(block.height);
|
||||
this.stateService.addBlock(block);
|
||||
this.stateService.txConfirmed$.next([response.txConfirmed, block]);
|
||||
});
|
||||
this.stateService.updateChainTip(response.block.height);
|
||||
this.stateService.addBlock(response.block);
|
||||
this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
|
||||
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
||||
reinitBlocks = true;
|
||||
}
|
||||
|
||||
@ -935,15 +935,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
const tokenRelated = (flags & (TransactionFlags.inscription | TransactionFlags.op_return)) !== 0n;
|
||||
if (!addressReuse &&
|
||||
tx.vin.length >= 5 &&
|
||||
tx.vout.length >= 5 &&
|
||||
(Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 &&
|
||||
!tokenRelated &&
|
||||
tx.vin.length / tx.vout.length < 5 &&
|
||||
tx.vin.length / tx.vout.length > 0.2
|
||||
) {
|
||||
if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) {
|
||||
flags |= TransactionFlags.coinjoin;
|
||||
}
|
||||
// more than 5:1 input:output ratio
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "GoMining",
|
||||
"tags": ["GM", "GoMining"],
|
||||
"link": "https://gomining.com/",
|
||||
"slug": "gomining"
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user