Compare commits

...

31 Commits

Author SHA1 Message Date
wiz
419fe73edf
Merge pull request #6595 from mempool/wiz/remove-see-more-details-here
Fix 'see more details here' link in FAQ
2026-06-27 15:21:39 +09:00
wiz
446b5e84c3
Remove 'see more details here' link from FAQ 2026-06-27 14:52:21 +09:00
wiz
a35c764545
Merge pull request #6594 from mempool/mononaut/miner-json
add miner json
2026-06-27 13:51:04 +09:00
mononaut
7ca809a074
miner json fixes 2026-06-26 12:40:33 +00:00
mononaut
2f76744634
add miner json 2026-06-26 09:18:10 +00:00
wiz
9f26ca7b48
Merge pull request #6593 from mempool/mononaut/dmnd-tags
parse dmnd stratum v2 tags
2026-06-26 13:59:13 +09:00
mononaut
265fec4639
parse dmnd stratum v2 tags 2026-06-26 04:36:57 +00:00
mononaut
d63a8b6358
Merge pull request #6575 from mempool/mononaut/bump-angular
Some checks failed
Backend Integration Tests with MariaDB / Backend Integration Tests - node ${{ matrix.node }} (24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Cache assets for builds (24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Validate generated backend Docker JSON (push) Has been cancelled
Supply Chain Audit / Backend install-script audit (push) Has been cancelled
Supply Chain Audit / Frontend install-script audit (push) Has been cancelled
Docker build on tag / Test built Docker images (push) Has been cancelled
Docker build on tag / Tag release build as latest (frontend) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (liquid, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (mempool, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (testnet4, 24.13.0) (push) Has been cancelled
Docker build on tag / Build and push to DockerHub (backend) (push) Has been cancelled
Docker build on tag / Build and push to DockerHub (frontend) (push) Has been cancelled
Docker build on tag / Tag release build as latest (backend) (push) Has been cancelled
bump angular to 20.3.25
2026-06-18 16:58:51 +09:00
mononaut
eac66f3bd7
bump angular to 20.3.25 2026-06-18 06:05:49 +00:00
mononaut
5825044253
Merge pull request #6561 from mempool/rodribp/pagination-stale-blocks
add: Inifinite scrolling to blocks/stale page
2026-06-18 13:19:06 +09:00
mononaut
f654704169
fix missing canonical hash on stale block responses 2026-06-17 20:42:49 -06:00
mononaut
2db57928c4
increase stale tip page size to 25 2026-06-17 20:42:49 -06:00
mononaut
d3317a2e8b
replace staleTip cache with stale block cache 2026-06-17 20:42:48 -06:00
mononaut
fd3eee0d30
handle stale-tips pagination errors 2026-06-17 20:42:47 -06:00
mononaut
9c1fab54c1
remove unnecessary branchlen increments/decrements 2026-06-17 20:42:47 -06:00
rodribp
e1fe572549
change to always get staleTips from memory 2026-06-17 20:42:46 -06:00
rodribp
a8b1798eda
fix: fetch chain tips from db, not all blocks 2026-06-17 20:42:46 -06:00
Rodrigo Bonilla
8733ce74a0
typo
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-17 20:42:45 -06:00
rodribp
8081393435
add: Inifinite scrolling to blocks/stale page 2026-06-17 20:42:44 -06:00
mononaut
6bb896eea2
Merge pull request #6577 from mempool/dependabot/npm_and_yarn/frontend/hono-4.12.25
Some checks failed
Backend Integration Tests with MariaDB / Backend Integration Tests - node ${{ matrix.node }} (24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Cache assets for builds (24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Validate generated backend Docker JSON (push) Has been cancelled
Supply Chain Audit / Backend install-script audit (push) Has been cancelled
Supply Chain Audit / Frontend install-script audit (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (liquid, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (mempool, 24.13.0) (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (testnet4, 24.13.0) (push) Has been cancelled
Bump hono from 4.12.16 to 4.12.25 in /frontend
2026-06-17 12:20:19 +09:00
dependabot[bot]
5d45a51110
Bump hono from 4.12.16 to 4.12.25 in /frontend
Bumps [hono](https://github.com/honojs/hono) from 4.12.16 to 4.12.25.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.16...v4.12.25)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 02:39:29 +00:00
mononaut
a5db030b21
Merge pull request #6485 from mempool/dependabot/npm_and_yarn/frontend/noble/secp256k1-3.1.0
Bump @noble/secp256k1 from 3.0.0 to 3.1.0 in /frontend
2026-06-17 11:38:25 +09:00
dependabot[bot]
7680163d68
Bump @noble/secp256k1 from 3.0.0 to 3.1.0 in /frontend
Bumps [@noble/secp256k1](https://github.com/paulmillr/noble-secp256k1) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/paulmillr/noble-secp256k1/releases)
- [Commits](https://github.com/paulmillr/noble-secp256k1/compare/3.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: "@noble/secp256k1"
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 02:09:38 +00:00
mononaut
15bc471ff2
Merge pull request #6548 from mempool/rodribp/fix-false-coinjoin-flag
Some checks are pending
Backend Integration Tests with MariaDB / Backend Integration Tests - node ${{ matrix.node }} (24.13.0) (push) Waiting to run
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Waiting to run
CI Pipeline for the Backend and Frontend / Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Waiting to run
CI Pipeline for the Backend and Frontend / Cache assets for builds (24.13.0) (push) Waiting to run
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (dev, 24.13.0) (push) Blocked by required conditions
CI Pipeline for the Backend and Frontend / Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} (prod, 24.13.0) (push) Blocked by required conditions
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (liquid, 24.13.0) (push) Blocked by required conditions
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (mempool, 24.13.0) (push) Blocked by required conditions
CI Pipeline for the Backend and Frontend / E2E tests for ${{ matrix.module }} (testnet4, 24.13.0) (push) Blocked by required conditions
CI Pipeline for the Backend and Frontend / Validate generated backend Docker JSON (push) Waiting to run
Supply Chain Audit / Backend install-script audit (push) Waiting to run
Supply Chain Audit / Frontend install-script audit (push) Waiting to run
fix: False positive coinjoin (#6464)
2026-06-16 19:04:28 +09:00
mononaut
db5df69fb0
Merge pull request #6574 from mempool/dependabot/npm_and_yarn/frontend/js-yaml-4.2.0
Bump js-yaml from 4.1.1 to 4.2.0 in /frontend
2026-06-16 18:06:27 +09:00
dependabot[bot]
7469394bf8
Bump js-yaml from 4.1.1 to 4.2.0 in /frontend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 08:29:38 +00:00
mononaut
4a90223792
Merge pull request #6573 from mempool/dependabot/npm_and_yarn/frontend/launch-editor-2.14.1
Bump launch-editor from 2.12.0 to 2.14.1 in /frontend
2026-06-16 17:27:19 +09:00
rodribp
bf60cf721e
fix: False positive coinjoin (#6464) 2026-06-16 02:24:18 -06:00
dependabot[bot]
85ad3dee3a
Bump launch-editor from 2.12.0 to 2.14.1 in /frontend
Bumps [launch-editor](https://github.com/vitejs/launch-editor) from 2.12.0 to 2.14.1.
- [Commits](https://github.com/vitejs/launch-editor/compare/v2.12.0...v2.14.1)

---
updated-dependencies:
- dependency-name: launch-editor
  dependency-version: 2.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 08:20:21 +00:00
mononaut
f723a5a385
Merge pull request #6572 from mempool/dependabot/npm_and_yarn/frontend/tar-7.5.16
Bump tar from 7.5.11 to 7.5.16 in /frontend
2026-06-16 17:17:37 +09:00
dependabot[bot]
90b0dcf204
Bump tar from 7.5.11 to 7.5.16 in /frontend
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.11 to 7.5.16.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.11...v7.5.16)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 23:53:07 +00:00
22 changed files with 620 additions and 513 deletions

View File

@ -61,6 +61,7 @@ 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
@ -628,14 +629,18 @@ class BitcoinRoutes {
private async getStaleTips(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet', 'testnet4', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
const tips = await chainTips.getStaleTips();
if (tips.length > 0) {
res.json(tips);
} else {
handleError(req, res, 503, `Temporarily unavailable`);
return;
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);
} else { // Liquid
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;

View File

@ -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 } from '../utils/bitcoin-script';
import { parseDATUMTemplateCreator, parseDMNDTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
import { getBlockFirstSeenFromLogs, getOldestLogTimestampFromLogs, scanLogsForBlocksFirstSeen } from '../utils/file-read';
@ -359,6 +359,8 @@ 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);
}
}

View File

@ -2,6 +2,7 @@ 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';
@ -23,26 +24,29 @@ 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 staleTips: Record<number, StaleTip> = {};
private validChainTips: ChainTip[] = []; // 'valid-fork' and 'valid-headers' only, in descending height order
private staleBlocks: Record<string, BlockExtended> = {};
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 staleTipsCacheSize = 50;
private staleBlocksCacheSize = 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;
@ -69,6 +73,7 @@ class ChainTips {
orphan = {
height: block.height,
hash: block.id,
branchlen: chain.branchlen,
status: chain.status,
prevhash: block.previousblockhash,
};
@ -119,13 +124,7 @@ class ChainTips {
this.orphansByHeight[orphan.height].push(orphan);
}
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();
this.trimStaleBlocksCache();
// index new orphaned blocks in the background
void this.$indexOrphanedBlocks();
@ -157,7 +156,7 @@ class ChainTips {
}
let staleBlock: BlockExtended | undefined;
const alreadyIndexed = await BlocksSummariesRepository.$isSummaryIndexed(block.id);
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];
const needToCache = this.shouldCacheStaleBlock(block.id, block.height);
if (!alreadyIndexed) {
staleBlock = await blocks.$indexBlock(block.id, block, true);
await blocks.$indexBlockSummary(block.id, block.height, true);
@ -168,16 +167,9 @@ class ChainTips {
}
if (staleBlock && needToCache) {
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();
// ensure the canonical block is correctly indexed
await blocks.$indexBlockByHeight(staleBlock.height);
this.cacheStaleBlock(staleBlock);
}
} catch (e) {
logger.err(`Failed to index orphaned block ${block?.id} at height ${block?.height}. Reason: ${e instanceof Error ? e.message : e}`);
@ -186,12 +178,42 @@ class ChainTips {
this.indexingOrphanedBlocks = false;
}
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];
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];
}
}
}
@ -208,8 +230,51 @@ class ChainTips {
return this.chainTips;
}
public getStaleTips(): StaleTip[] {
return Object.values(this.staleTips).sort((a, b) => b.height - a.height);
/**
* 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;
}
clearOrphanCacheAboveHeight(height: number): void {
@ -234,4 +299,4 @@ class ChainTips {
}
}
export default new ChainTips();
export default new ChainTips();

View File

@ -749,7 +749,15 @@ 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;
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 ) {
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
) {
flags |= TransactionFlags.coinjoin;
}
// more than 5:1 input:output ratio

View File

@ -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 } from '../utils/bitcoin-script';
import { parseDATUMTemplateCreator, parseDMNDTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock {
@ -575,8 +575,34 @@ 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 {
@ -1256,6 +1282,10 @@ 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;
@ -1344,6 +1374,8 @@ 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;

View File

@ -224,4 +224,27 @@ 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 [];
}
}

View File

@ -10,24 +10,24 @@
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@angular-devkit/build-angular": "^20.3.25",
"@angular/animations": "^20.3.19",
"@angular/animations": "^20.3.25",
"@angular/cli": "^20.3.25",
"@angular/common": "^20.3.19",
"@angular/compiler": "^20.3.19",
"@angular/core": "^20.3.19",
"@angular/forms": "^20.3.19",
"@angular/localize": "^20.3.19",
"@angular/platform-browser": "^20.3.19",
"@angular/platform-browser-dynamic": "^20.3.19",
"@angular/platform-server": "^20.3.19",
"@angular/router": "^20.3.19",
"@angular/common": "^20.3.25",
"@angular/compiler": "^20.3.25",
"@angular/core": "^20.3.25",
"@angular/forms": "^20.3.25",
"@angular/localize": "^20.3.25",
"@angular/platform-browser": "^20.3.25",
"@angular/platform-browser-dynamic": "^20.3.25",
"@angular/platform-server": "^20.3.25",
"@angular/router": "^20.3.25",
"@angular/ssr": "^20.3.25",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
"@noble/secp256k1": "^3.0.0",
"@noble/secp256k1": "^3.1.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~5.3.8",
"clipboard": "^2.0.11",
@ -41,8 +41,8 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/compiler-cli": "^20.3.19",
"@angular/language-service": "^20.3.19",
"@angular/compiler-cli": "^20.3.25",
"@angular/language-service": "^20.3.25",
"@types/node": "^24.9.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
@ -354,23 +354,6 @@
}
}
},
"node_modules/@angular-devkit/architect/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/architect/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -389,21 +372,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular-devkit/architect/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/architect/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -1021,23 +989,6 @@
}
}
},
"node_modules/@angular-devkit/build-angular/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -1156,21 +1107,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -1307,23 +1243,6 @@
}
}
},
"node_modules/@angular-devkit/schematics/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/schematics/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -1342,21 +1261,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular-devkit/schematics/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular-devkit/schematics/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -1367,9 +1271,10 @@
}
},
"node_modules/@angular/animations": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.19.tgz",
"integrity": "sha512-/FjU9i7J58/yBURhgVSIiLDcuyOfJxAa0b7ZrOsx6P+FES+M2T2BKZl5V2NuiP2fDFtjsV7U+M/Z9UNUmeHCEw==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.25.tgz",
"integrity": "sha512-lQmti3tI85D525TjUVGqCNLzFxSUoZg+vgIyvuGJZPY0UU/o2S6KAxW6ObmcRotZZHNfenLHIxWgzamBDjIjuw==",
"deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1378,7 +1283,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "20.3.19"
"@angular/core": "20.3.25"
}
},
"node_modules/@angular/build": {
@ -2240,23 +2145,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@angular/cli/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
@ -2332,21 +2220,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/cli/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -2434,9 +2307,9 @@
}
},
"node_modules/@angular/common": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz",
"integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.25.tgz",
"integrity": "sha512-rnRGcXbjet0DHgkRL4Dqxk21G2T4UypVfiTV/fay58H8w9U89PJ1L6gRmk8B/uyfpii/9r23cBwnpcguQykxYw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2445,14 +2318,14 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "20.3.19",
"@angular/core": "20.3.25",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz",
"integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.25.tgz",
"integrity": "sha512-TSh6gVoQqlLPqWwsYMK0lfVEQYENQO+USzS+BHFXEHFfgBRap6qDpIUGnRdj0Y2PlaVJUVFbeq1855EZUPUEoA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2462,9 +2335,9 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.19.tgz",
"integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.25.tgz",
"integrity": "sha512-iqxwVo5Pgzt3EfT49OZ6plxA6KKxwv7ixx1XNH7QRvaOJC9gmsPScWpx+LO7ZsVZdo/NkA+rnXDl0PauUgGciw==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.3",
@ -2484,7 +2357,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "20.3.19",
"@angular/compiler": "20.3.25",
"typescript": ">=5.8 <6.0"
},
"peerDependenciesMeta": {
@ -2522,9 +2395,9 @@
}
},
"node_modules/@angular/core": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz",
"integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.25.tgz",
"integrity": "sha512-B4XnnR5jzikZDvZ4PjwjAWZMT14dxrKrmJdwa/n0yp7rMPkIJTKF6ZJMg4d1pLWLLSsc2oWHioN3UrWlGqIKnA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2533,7 +2406,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "20.3.19",
"@angular/compiler": "20.3.25",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0"
},
@ -2547,9 +2420,9 @@
}
},
"node_modules/@angular/forms": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.19.tgz",
"integrity": "sha512-WJotd+Lhl4FG2b0K+aQNyQDHhR515zKCuphjiUqEW7sifWrOQxANLKzPBngGrH75ayANFgPaDf7U3ZRIoblcQA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.25.tgz",
"integrity": "sha512-vGRo1LVPFo2Cu0k+QyDTlsBv5UbN0c3Et2YMS+43oyi1c4keocntBccOjLyM5C0kpMz4+pP81MqqYpAWu2k+TQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2558,16 +2431,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.19",
"@angular/core": "20.3.19",
"@angular/platform-browser": "20.3.19",
"@angular/common": "20.3.25",
"@angular/core": "20.3.25",
"@angular/platform-browser": "20.3.25",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/language-service": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.3.19.tgz",
"integrity": "sha512-9J0XrAKXInz11KKyNMrMZmn2NSjVbxzt/DsAumbrzzixeZwiY7vDy2Kqw/LLFLi7IlfMQ/gznz/mCVVgUWI5Gg==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.3.25.tgz",
"integrity": "sha512-3PZUwbDUVQXk9BLSiNUpDxTnRXGR3szwK9QFQaGDt8lhmLYaQLh3VJsY0FHDRghHZYIGR2P2MbVxGQHytF+IPw==",
"dev": true,
"license": "MIT",
"engines": {
@ -2575,9 +2448,9 @@
}
},
"node_modules/@angular/localize": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.19.tgz",
"integrity": "sha512-bXOwxzJUvHzmADI6czdjYnuBMil/UK3CW1dfbrC1MrlLtD0R7g4YZs08J7aWXd+/A4LmTPfkpZhI5ApxGAL0Tg==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.25.tgz",
"integrity": "sha512-N4wmEBH44h58Av+1ivQ7LAe6Q0QP/2oBngvmnkO39AM6tadlnQGmaOvVzCUZe4BpOWYrrXQNiszSxGD3mUGgHg==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.3",
@ -2594,14 +2467,14 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "20.3.19",
"@angular/compiler-cli": "20.3.19"
"@angular/compiler": "20.3.25",
"@angular/compiler-cli": "20.3.25"
}
},
"node_modules/@angular/platform-browser": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz",
"integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.25.tgz",
"integrity": "sha512-0k06U/AJRQifGMLkcU3R9uEHWbuKEzkKMuKcGagXTrkeFvCG2Ub4JdsbcjFNWB2bspWgaxIMSceuj7c83U5wOA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2610,9 +2483,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/animations": "20.3.19",
"@angular/common": "20.3.19",
"@angular/core": "20.3.19"
"@angular/animations": "20.3.25",
"@angular/common": "20.3.25",
"@angular/core": "20.3.25"
},
"peerDependenciesMeta": {
"@angular/animations": {
@ -2621,9 +2494,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.19.tgz",
"integrity": "sha512-OgErw7wjcC+8yKF5h99hJq8x+tvc091wThfmdL5YC+U3HgRmUaNZFgB/jR7cb/NeeeC42QW5Vc0qoUTC9rMnLQ==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.25.tgz",
"integrity": "sha512-3Ku+IsN4tQPVBsw75SoLbLf7TsXAGL0rGPHSsyNYFhG2ZZeQuYNIAi8mc4cwz/qMDnuassHFrCxuLDgN6Yab5w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2632,16 +2505,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.19",
"@angular/compiler": "20.3.19",
"@angular/core": "20.3.19",
"@angular/platform-browser": "20.3.19"
"@angular/common": "20.3.25",
"@angular/compiler": "20.3.25",
"@angular/core": "20.3.25",
"@angular/platform-browser": "20.3.25"
}
},
"node_modules/@angular/platform-server": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.19.tgz",
"integrity": "sha512-9STNB8Z5uYpaIgzfiJOH81c4CY2lM3oq/650+pdnjJsedxyEi+NAbnn5tF857Cd/N+43lR+OMolKgm0MJziHqw==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.25.tgz",
"integrity": "sha512-uOpLILe5QP9WLXhwshA3fbHc+Wx/4nrUBZNns/dw3t06bRHMbbkutF8eVs+n6Atl332y4mOcStzMX1ilBUSFHw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0",
@ -2651,17 +2524,17 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.19",
"@angular/compiler": "20.3.19",
"@angular/core": "20.3.19",
"@angular/platform-browser": "20.3.19",
"@angular/common": "20.3.25",
"@angular/compiler": "20.3.25",
"@angular/core": "20.3.25",
"@angular/platform-browser": "20.3.25",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/router": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.19.tgz",
"integrity": "sha512-qHrMniHOsCJ4neZmcQVodjutJilyXAXk7EhLa931QyL0qyVKVomv6E0I3UFzRaC3ZeHc+hzBdU6C6bvMFKTl1g==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.25.tgz",
"integrity": "sha512-YIjLHWAufTaukNj15hEoys29e7XNhnCRsS1/95h/OqR69R3adbB8hV7ut7gO6XdXokriYqb4gtoUjoESxR+xFQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -2670,9 +2543,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "20.3.19",
"@angular/core": "20.3.19",
"@angular/platform-browser": "20.3.19",
"@angular/common": "20.3.25",
"@angular/core": "20.3.25",
"@angular/platform-browser": "20.3.25",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -6591,10 +6464,9 @@
}
},
"node_modules/@noble/secp256k1": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==",
"license": "MIT",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
@ -7583,23 +7455,6 @@
}
}
},
"node_modules/@schematics/angular/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -7618,21 +7473,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@schematics/angular/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -12527,10 +12367,9 @@
}
},
"node_modules/hono": {
"version": "4.12.16",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
"license": "MIT",
"version": "4.12.25",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
"engines": {
"node": ">=16.9.0"
}
@ -13277,10 +13116,19 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"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"
}
],
"dependencies": {
"argparse": "^2.0.1"
},
@ -13435,12 +13283,12 @@
}
},
"node_modules/launch-editor": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
"integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
"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==",
"dependencies": {
"picocolors": "^1.1.1",
"shell-quote": "^1.8.3"
"shell-quote": "^1.8.4"
}
},
"node_modules/lazy-ass": {
@ -17290,9 +17138,9 @@
}
},
"node_modules/tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
@ -18997,16 +18845,6 @@
"ajv": "^8.0.0"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"optional": true,
"peer": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -19017,13 +18855,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"optional": true,
"peer": true
},
"source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -19281,16 +19112,6 @@
"ajv": "^8.0.0"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"optional": true,
"peer": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -19371,13 +19192,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"optional": true,
"peer": true
},
"semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -19454,16 +19268,6 @@
"ajv": "^8.0.0"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"optional": true,
"peer": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -19474,13 +19278,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"optional": true,
"peer": true
},
"source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -19489,9 +19286,9 @@
}
},
"@angular/animations": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.19.tgz",
"integrity": "sha512-/FjU9i7J58/yBURhgVSIiLDcuyOfJxAa0b7ZrOsx6P+FES+M2T2BKZl5V2NuiP2fDFtjsV7U+M/Z9UNUmeHCEw==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.25.tgz",
"integrity": "sha512-lQmti3tI85D525TjUVGqCNLzFxSUoZg+vgIyvuGJZPY0UU/o2S6KAxW6ObmcRotZZHNfenLHIxWgzamBDjIjuw==",
"requires": {
"tslib": "^2.3.0"
}
@ -19890,16 +19687,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"optional": true,
"peer": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
@ -19947,13 +19734,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"optional": true,
"peer": true
},
"semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -20004,25 +19784,25 @@
}
},
"@angular/common": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz",
"integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.25.tgz",
"integrity": "sha512-rnRGcXbjet0DHgkRL4Dqxk21G2T4UypVfiTV/fay58H8w9U89PJ1L6gRmk8B/uyfpii/9r23cBwnpcguQykxYw==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/compiler": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz",
"integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.25.tgz",
"integrity": "sha512-TSh6gVoQqlLPqWwsYMK0lfVEQYENQO+USzS+BHFXEHFfgBRap6qDpIUGnRdj0Y2PlaVJUVFbeq1855EZUPUEoA==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/compiler-cli": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.19.tgz",
"integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.25.tgz",
"integrity": "sha512-iqxwVo5Pgzt3EfT49OZ6plxA6KKxwv7ixx1XNH7QRvaOJC9gmsPScWpx+LO7ZsVZdo/NkA+rnXDl0PauUgGciw==",
"requires": {
"@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14",
@ -20050,31 +19830,31 @@
}
},
"@angular/core": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz",
"integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.25.tgz",
"integrity": "sha512-B4XnnR5jzikZDvZ4PjwjAWZMT14dxrKrmJdwa/n0yp7rMPkIJTKF6ZJMg4d1pLWLLSsc2oWHioN3UrWlGqIKnA==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/forms": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.19.tgz",
"integrity": "sha512-WJotd+Lhl4FG2b0K+aQNyQDHhR515zKCuphjiUqEW7sifWrOQxANLKzPBngGrH75ayANFgPaDf7U3ZRIoblcQA==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.25.tgz",
"integrity": "sha512-vGRo1LVPFo2Cu0k+QyDTlsBv5UbN0c3Et2YMS+43oyi1c4keocntBccOjLyM5C0kpMz4+pP81MqqYpAWu2k+TQ==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/language-service": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.3.19.tgz",
"integrity": "sha512-9J0XrAKXInz11KKyNMrMZmn2NSjVbxzt/DsAumbrzzixeZwiY7vDy2Kqw/LLFLi7IlfMQ/gznz/mCVVgUWI5Gg==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.3.25.tgz",
"integrity": "sha512-3PZUwbDUVQXk9BLSiNUpDxTnRXGR3szwK9QFQaGDt8lhmLYaQLh3VJsY0FHDRghHZYIGR2P2MbVxGQHytF+IPw==",
"dev": true
},
"@angular/localize": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.19.tgz",
"integrity": "sha512-bXOwxzJUvHzmADI6czdjYnuBMil/UK3CW1dfbrC1MrlLtD0R7g4YZs08J7aWXd+/A4LmTPfkpZhI5ApxGAL0Tg==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.25.tgz",
"integrity": "sha512-N4wmEBH44h58Av+1ivQ7LAe6Q0QP/2oBngvmnkO39AM6tadlnQGmaOvVzCUZe4BpOWYrrXQNiszSxGD3mUGgHg==",
"requires": {
"@babel/core": "7.28.3",
"@types/babel__core": "7.20.5",
@ -20083,34 +19863,34 @@
}
},
"@angular/platform-browser": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz",
"integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.25.tgz",
"integrity": "sha512-0k06U/AJRQifGMLkcU3R9uEHWbuKEzkKMuKcGagXTrkeFvCG2Ub4JdsbcjFNWB2bspWgaxIMSceuj7c83U5wOA==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/platform-browser-dynamic": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.19.tgz",
"integrity": "sha512-OgErw7wjcC+8yKF5h99hJq8x+tvc091wThfmdL5YC+U3HgRmUaNZFgB/jR7cb/NeeeC42QW5Vc0qoUTC9rMnLQ==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.25.tgz",
"integrity": "sha512-3Ku+IsN4tQPVBsw75SoLbLf7TsXAGL0rGPHSsyNYFhG2ZZeQuYNIAi8mc4cwz/qMDnuassHFrCxuLDgN6Yab5w==",
"requires": {
"tslib": "^2.3.0"
}
},
"@angular/platform-server": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.19.tgz",
"integrity": "sha512-9STNB8Z5uYpaIgzfiJOH81c4CY2lM3oq/650+pdnjJsedxyEi+NAbnn5tF857Cd/N+43lR+OMolKgm0MJziHqw==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.25.tgz",
"integrity": "sha512-uOpLILe5QP9WLXhwshA3fbHc+Wx/4nrUBZNns/dw3t06bRHMbbkutF8eVs+n6Atl332y4mOcStzMX1ilBUSFHw==",
"requires": {
"tslib": "^2.3.0",
"xhr2": "^0.2.0"
}
},
"@angular/router": {
"version": "20.3.19",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.19.tgz",
"integrity": "sha512-qHrMniHOsCJ4neZmcQVodjutJilyXAXk7EhLa931QyL0qyVKVomv6E0I3UFzRaC3ZeHc+hzBdU6C6bvMFKTl1g==",
"version": "20.3.25",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.25.tgz",
"integrity": "sha512-YIjLHWAufTaukNj15hEoys29e7XNhnCRsS1/95h/OqR69R3adbB8hV7ut7gO6XdXokriYqb4gtoUjoESxR+xFQ==",
"requires": {
"tslib": "^2.3.0"
}
@ -22329,9 +22109,9 @@
"requires": {}
},
"@noble/secp256k1": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
@ -22832,16 +22612,6 @@
"ajv": "^8.0.0"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"optional": true,
"peer": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -22852,13 +22622,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"optional": true,
"peer": true
},
"source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@ -26316,9 +26079,9 @@
}
},
"hono": {
"version": "4.12.16",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
"integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="
"version": "4.12.25",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="
},
"hosted-git-info": {
"version": "9.0.2",
@ -26822,9 +26585,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"requires": {
"argparse": "^2.0.1"
}
@ -26946,12 +26709,12 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"launch-editor": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
"integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
"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==",
"requires": {
"picocolors": "^1.1.1",
"shell-quote": "^1.8.3"
"shell-quote": "^1.8.4"
}
},
"lazy-ass": {
@ -29613,9 +29376,9 @@
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="
},
"tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"requires": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",

View File

@ -60,17 +60,17 @@
},
"dependencies": {
"@angular-devkit/build-angular": "^20.3.25",
"@angular/animations": "^20.3.19",
"@angular/animations": "^20.3.25",
"@angular/cli": "^20.3.25",
"@angular/common": "^20.3.19",
"@angular/compiler": "^20.3.19",
"@angular/core": "^20.3.19",
"@angular/forms": "^20.3.19",
"@angular/localize": "^20.3.19",
"@angular/platform-browser": "^20.3.19",
"@angular/platform-browser-dynamic": "^20.3.19",
"@angular/platform-server": "^20.3.19",
"@angular/router": "^20.3.19",
"@angular/common": "^20.3.25",
"@angular/compiler": "^20.3.25",
"@angular/core": "^20.3.25",
"@angular/forms": "^20.3.25",
"@angular/localize": "^20.3.25",
"@angular/platform-browser": "^20.3.25",
"@angular/platform-browser-dynamic": "^20.3.25",
"@angular/platform-server": "^20.3.25",
"@angular/router": "^20.3.25",
"@angular/ssr": "^20.3.25",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-common-types": "~6.7.2",
@ -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.0.0",
"@noble/secp256k1": "^3.1.0",
"bootstrap": "~5.3.8",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
@ -91,8 +91,8 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/compiler-cli": "^20.3.19",
"@angular/language-service": "^20.3.19",
"@angular/compiler-cli": "^20.3.25",
"@angular/language-service": "^20.3.25",
"@types/node": "^24.9.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",

View File

@ -53,15 +53,23 @@
<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;">
<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-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>
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">

View File

@ -188,14 +188,21 @@
<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;">
<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-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>
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">

View File

@ -79,8 +79,14 @@
<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">
<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 *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>
</ng-container>
<ng-template #centralisedPool>
<app-mining-pool [slug]="block.extras.pool.slug" [name]="block.extras.pool.name"></app-mining-pool>

View File

@ -4,7 +4,7 @@
<div class="clearfix"></div>
<div class="chain-tips" [ngStyle]="{ 'min-height': '295px', 'opacity': isLoading ? '0.75' : '1' }">
<div class="chain-tips" infiniteScroll [alwaysCallback]="true" [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" (scrolled)="loadMore()" [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 + 1 }}</span>
<span class="badge bg-secondary depth-badge" i18n="chain-tips.depth">Depth {{ chainTip.branchlen }}</span>
</span>
</p>
<div class="stale-tip-wrapper">
@ -57,8 +57,14 @@
<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">
<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 *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>
</ng-container>
<ng-template #staleBlockCentralisedPool>
<app-mining-pool [slug]="chainTip.stale.extras.pool.slug" [name]="chainTip.stale.extras.pool.name"></app-mining-pool>
@ -103,8 +109,14 @@
<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">
<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 *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>
</ng-container>
<ng-template #canonicalBlockCentralisedPool>
<app-mining-pool [slug]="chainTip.canonical.extras.pool.slug" [name]="chainTip.canonical.extras.pool.name"></app-mining-pool>
@ -116,10 +128,19 @@
</div>
</div>
<div class="no-chain-tips" *ngIf="!chainTips?.length">
<div class="no-chain-tips" *ngIf="!chainTips?.length && !isLoading && !error">
<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>

View File

@ -1,6 +1,6 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, timer } from 'rxjs';
import { catchError, map, retry, share, switchMap, 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,9 +16,12 @@ import { seoDescriptionNetwork } from '@app/shared/common.utils';
})
export class StaleList implements OnInit {
chainTips$: Observable<StaleTip[]>;
nextChainTipSubject = new BehaviorSubject(null);
urlFragmentSubscription: Subscription;
loadMoreSubject = new BehaviorSubject<number | undefined>(undefined);
chainTips: StaleTip[] = [];
isLoading = true;
isLoadingMore = false;
fullyLoaded = false;
error: unknown = null;
gradientColors = {
'': ['var(--mainnet-alt)', 'var(--primary)'],
@ -37,32 +40,73 @@ export class StaleList implements OnInit {
) { }
ngOnInit(): void {
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;
}),
tap(() => {
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;
}),
map(() => this.chainTips),
share(),
);
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)';

View File

@ -333,14 +333,21 @@
@if (pool) {
<td class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: var(--fg);padding:0;">
<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-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>
</a>
</td>
} @else {

View File

@ -45,6 +45,8 @@ export interface Pool {
id: number;
name: string;
slug: string;
minerName?: string;
minerSlug?: string;
minerNames: string[] | null;
}

View File

@ -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 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>
<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>
</ng-template>
<ng-template type="why-dont-fee-ranges-match">

View File

@ -224,6 +224,8 @@ export interface BlockExtension {
id: number;
name: string;
slug: string;
minerName?: string;
minerSlug?: string;
minerNames: string[] | null;
}
orphans?: {

View File

@ -2,13 +2,14 @@ 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, take, tap } from 'rxjs';
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, switchMap, 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'
@ -24,7 +25,8 @@ export class ApiService {
constructor(
private httpClient: HttpClient,
private stateService: StateService,
private storageService: StorageService
private storageService: StorageService,
private minersService: MinersService,
) {
this.apiBaseUrl = ''; // use relative URL by default
if (!stateService.isBrowser) { // except when inside AU SSR process
@ -172,8 +174,10 @@ export class ApiService {
return this.httpClient.get<ChainTip[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/chain-tips');
}
getStaleTips$(): Observable<StaleTip[]> {
return this.httpClient.get<StaleTip[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/stale-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))
);
}
liquidPegs$(): Observable<CurrentPegs> {
@ -308,6 +312,8 @@ 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))
);
}
@ -315,11 +321,15 @@ 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);
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash).pipe(
switchMap((block) => this.minersService.applyBlockMinerDetails$(block))
);
}
getBlockDataFromTimestamp$(timestamp: number): Observable<any> {

View File

@ -0,0 +1,74 @@
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;
}
}

View File

@ -3,12 +3,13 @@ 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, Subscription } from 'rxjs';
import { firstValueFrom, of, Subscription } from 'rxjs';
import { ApiService } from '@app/services/api.service';
import { take } from 'rxjs/operators';
import { catchError, 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;
@ -51,6 +52,7 @@ export class WebsocketService {
private apiService: ApiService,
private transferState: TransferState,
private cacheService: CacheService,
private minersService: MinersService,
) {
if (!this.stateService.isBrowser) {
// @ts-ignore
@ -356,9 +358,14 @@ export class WebsocketService {
if (response.blocks && response.blocks.length) {
const blocks = response.blocks;
this.stateService.resetBlocks(blocks);
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
this.stateService.updateChainTip(maxHeight);
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);
});
}
if (response.tx) {
@ -371,9 +378,14 @@ export class WebsocketService {
if (response.block) {
if (response.block.height === this.stateService.latestBlockHeight + 1) {
this.stateService.updateChainTip(response.block.height);
this.stateService.addBlock(response.block);
this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
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]);
});
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
reinitBlocks = true;
}

View File

@ -935,7 +935,15 @@ 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;
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 ) {
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
) {
flags |= TransactionFlags.coinjoin;
}
// more than 5:1 input:output ratio

View File

@ -0,0 +1,8 @@
[
{
"name": "GoMining",
"tags": ["GM", "GoMining"],
"link": "https://gomining.com/",
"slug": "gomining"
}
]