Merge branch 'master' into natsoni/fix-unnecessary-load-more

This commit is contained in:
softsimon 2024-05-13 17:09:50 +07:00 committed by GitHub
commit d02625eb0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
172 changed files with 3369 additions and 929 deletions

View File

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

View File

@ -1,4 +1,4 @@
import { IBitcoinApi } from './bitcoin-api.interface'; import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@ -22,6 +22,7 @@ export interface AbstractBitcoinApi {
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>; $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;

View File

@ -205,3 +205,16 @@ export namespace IBitcoinApi {
"utxo_size_inc": number; "utxo_size_inc": number;
} }
} }
export interface TestMempoolAcceptResult {
txid: string,
wtxid: string,
allowed?: boolean,
vsize?: number,
fees?: {
base: number,
"effective-feerate": number,
"effective-includes": string[],
},
['reject-reason']?: string,
}

View File

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface'; import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; import mempool from '../mempool';
@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.sendRawTransaction(rawTransaction); return this.bitcoindClient.sendRawTransaction(rawTransaction);
} }
async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
if (rawTransactions.length) {
return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
} else {
return [];
}
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {

View File

@ -55,6 +55,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
@ -749,6 +750,19 @@ class BitcoinRoutes {
} }
} }
private async $testTransactions(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
} }
export default new BitcoinRoutes(); export default new BitcoinRoutes();

View File

@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }

View File

@ -839,8 +839,11 @@ class Blocks {
} else { } else {
this.currentBlockHeight++; this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`); logger.debug(`New block found (#${this.currentBlockHeight})!`);
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`); // skip updating the orphan block cache if we've fallen behind the chain tip
await chainTips.updateOrphanedBlocks(); if (this.currentBlockHeight >= blockHeightTip - 2) {
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks();
}
} }
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);

View File

@ -12,32 +12,68 @@ export interface OrphanedBlock {
height: number; height: number;
hash: string; hash: string;
status: 'valid-fork' | 'valid-headers' | 'headers-only'; status: 'valid-fork' | 'valid-headers' | 'headers-only';
prevhash: string;
} }
class ChainTips { class ChainTips {
private chainTips: ChainTip[] = []; private chainTips: ChainTip[] = [];
private orphanedBlocks: OrphanedBlock[] = []; private orphanedBlocks: { [hash: string]: OrphanedBlock } = {};
private blockCache: { [hash: string]: OrphanedBlock } = {};
private orphansByHeight: { [height: number]: OrphanedBlock[] } = {};
public async updateOrphanedBlocks(): Promise<void> { public async updateOrphanedBlocks(): Promise<void> {
try { try {
this.chainTips = await bitcoinClient.getChainTips(); this.chainTips = await bitcoinClient.getChainTips();
this.orphanedBlocks = [];
const start = Date.now();
const breakAt = start + 10000;
let newOrphans = 0;
this.orphanedBlocks = {};
for (const chain of this.chainTips) { for (const chain of this.chainTips) {
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') { if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
let block = await bitcoinClient.getBlock(chain.hash); const orphans: OrphanedBlock[] = [];
while (block && block.confirmations === -1) { let hash = chain.hash;
this.orphanedBlocks.push({ do {
height: block.height, let orphan = this.blockCache[hash];
hash: block.hash, if (!orphan) {
status: chain.status const block = await bitcoinClient.getBlock(hash);
}); if (block && block.confirmations === -1) {
block = await bitcoinClient.getBlock(block.previousblockhash); newOrphans++;
orphan = {
height: block.height,
hash: block.hash,
status: chain.status,
prevhash: block.previousblockhash,
};
this.blockCache[hash] = orphan;
}
}
if (orphan) {
orphans.push(orphan);
}
hash = orphan?.prevhash;
} while (hash && (Date.now() < breakAt));
for (const orphan of orphans) {
this.orphanedBlocks[orphan.hash] = orphan;
} }
} }
if (Date.now() >= breakAt) {
logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`);
break;
}
} }
logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`); this.orphansByHeight = {};
const allOrphans = Object.values(this.orphanedBlocks);
for (const orphan of allOrphans) {
if (!this.orphansByHeight[orphan.height]) {
this.orphansByHeight[orphan.height] = [];
}
this.orphansByHeight[orphan.height].push(orphan);
}
logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`);
} catch (e) { } catch (e) {
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
} }
@ -48,13 +84,7 @@ class ChainTips {
return []; return [];
} }
const orphans: OrphanedBlock[] = []; return this.orphansByHeight[height] || [];
for (const block of this.orphanedBlocks) {
if (block.height === height) {
orphans.push(block);
}
}
return orphans;
} }
} }

View File

@ -946,6 +946,33 @@ export class Common {
return this.validateTransactionHex(matches[1].toLowerCase()); return this.validateTransactionHex(matches[1].toLowerCase());
} }
static getTransactionsFromRequest(req: Request, limit: number = 25): string[] {
if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) {
throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 });
}
if (limit && req.body.length > limit) {
throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 });
}
const txs = req.body;
return txs.map(rawTx => {
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = /^((?:[a-fA-F0-9]{2})+)$/;
const matches = reg.exec(rawTx);
if (!matches || !matches[1]) {
throw Object.assign(new Error('Invalid hex string'), { code: -2 });
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this.validateTransactionHex(matches[1].toLowerCase());
});
}
private static validateTransactionHex(txhex: string): string { private static validateTransactionHex(txhex: string): string {
// Do not mutate txhex // Do not mutate txhex

View File

@ -666,7 +666,9 @@ class NodesApi {
node.last_update = null; node.last_update = null;
} }
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))];
const formattedSockets = (uniqueAddr.join(',')) ?? '';
const query = `INSERT INTO nodes( const query = `INSERT INTO nodes(
public_key, public_key,
first_seen, first_seen,
@ -695,13 +697,13 @@ class NodesApi {
node.alias, node.alias,
this.aliasToSearchText(node.alias), this.aliasToSearchText(node.alias),
node.color, node.color,
sockets, formattedSockets,
JSON.stringify(node.features), JSON.stringify(node.features),
node.last_update, node.last_update,
node.alias, node.alias,
this.aliasToSearchText(node.alias), this.aliasToSearchText(node.alias),
node.color, node.color,
sockets, formattedSockets,
JSON.stringify(node.features), JSON.stringify(node.features),
]); ]);
} catch (e) { } catch (e) {

View File

@ -404,6 +404,10 @@ class Mempool {
const newAccelerationMap: { [txid: string]: Acceleration } = {}; const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) { for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration; newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) { if (this.accelerations[acceleration.txid] == null) {
// new acceleration // new acceleration

View File

@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
@ -346,6 +347,17 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
if (parsedMessage['track-accelerations']) {
client['track-accelerations'] = true;
response['accelerations'] = JSON.stringify({
accelerations: Object.values(memPool.getAccelerations()),
});
} else {
client['track-accelerations'] = false;
}
}
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
this.updateSocketData(); this.updateSocketData();
@ -364,6 +376,18 @@ class WebsocketHandler {
client['track-donation'] = parsedMessage['track-donation']; client['track-donation'] = parsedMessage['track-donation'];
} }
if (parsedMessage['track-mempool-txids'] === true) {
client['track-mempool-txids'] = true;
} else if (parsedMessage['track-mempool-txids'] === false) {
delete client['track-mempool-txids'];
}
if (parsedMessage['track-mempool'] === true) {
client['track-mempool'] = true;
} else if (parsedMessage['track-mempool'] === false) {
delete client['track-mempool'];
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@ -524,6 +548,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
@ -545,6 +570,33 @@ class WebsocketHandler {
const latestTransactions = memPool.getLatestTransactions(); const latestTransactions = memPool.getLatestTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: newTransactions.map(tx => tx.txid),
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: newTransactions,
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions,
};
// update init data // update init data
const socketDataFields = { const socketDataFields = {
'mempoolInfo': mempoolInfo, 'mempoolInfo': mempoolInfo,
@ -604,9 +656,11 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions); const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions);
if (memPool.isInSync()) { // pre-compute acceleration delta
this.mempoolSequence++; const accelerationUpdate = {
} added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged // TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) { for (const server of this.webSocketServers) {
@ -847,6 +901,18 @@ class WebsocketHandler {
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
} }
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@ -992,6 +1058,31 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const txid of Object.keys(rbfTransactions)) {
for (const replaced of rbfTransactions[txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions,
};
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string { function getCachedResponse(key, data): string {
if (!responseCache[key]) { if (!responseCache[key]) {
@ -1000,10 +1091,6 @@ class WebsocketHandler {
return responseCache[key]; return responseCache[key];
} }
if (memPool.isInSync()) {
this.mempoolSequence++;
}
// TODO - Fix indentation after PR is merged // TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) { for (const server of this.webSocketServers) {
server.clients.forEach((client) => { server.clients.forEach((client) => {
@ -1185,6 +1272,14 @@ class WebsocketHandler {
} }
} }
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }

View File

@ -131,6 +131,7 @@ class Server {
}) })
.use(express.urlencoded({ extended: true })) .use(express.urlencoded({ extended: true }))
.use(express.text({ type: ['text/plain', 'application/base64'] })) .use(express.text({ type: ['text/plain', 'application/base64'] }))
.use(express.json())
; ;
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {

View File

@ -71,6 +71,22 @@ export interface MempoolBlockDelta {
changed: MempoolDeltaChange[]; changed: MempoolDeltaChange[];
} }
export interface MempoolDeltaTxids {
sequence: number,
added: string[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: string }[];
}
export interface MempoolDelta {
sequence: number,
added: MempoolTransactionExtended[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: TransactionExtended }[];
}
interface VinStrippedToScriptsig { interface VinStrippedToScriptsig {
scriptsig: string; scriptsig: string;
} }

View File

@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
RUN npm run build RUN npm run build
FROM nginx:1.25.4-alpine FROM nginx:1.26.0-alpine
WORKDIR /patch WORKDIR /patch

View File

@ -181,6 +181,11 @@
"bundleName": "wiz", "bundleName": "wiz",
"inject": false "inject": false
}, },
{
"input": "src/theme-bukele.scss",
"bundleName": "bukele",
"inject": false
},
"node_modules/@fortawesome/fontawesome-svg-core/styles.css" "node_modules/@fortawesome/fontawesome-svg-core/styles.css"
], ],
"vendorChunk": true, "vendorChunk": true,

View File

@ -1,30 +1,37 @@
{ {
"theme": "contrast", "theme": "bukele",
"enterprise": "onbtc", "enterprise": "onbtc",
"branding": { "branding": {
"name": "onbtc", "name": "onbtc",
"title": "Oficina Nacional del Bitcoin", "title": "Bitcoin Office",
"site_id": 19, "site_id": 19,
"header_img": "/resources/onbtc.svg", "header_img": "/resources/onbtclogo.svg",
"img": "/resources/elsalvador.svg", "footer_img": "/resources/onbtclogo.svg",
"rounded_corner": true "rounded_corner": true
}, },
"dashboard": { "dashboard": {
"widgets": [ "widgets": [
{ {
"component": "fees" "component": "fees",
"mobileOrder": 4
}, },
{ {
"component": "balance", "component": "balance",
"mobileOrder": 1,
"props": { "props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
} }
}, },
{ {
"component": "goggles" "component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "nayibbukele"
}
}, },
{ {
"component": "address", "component": "address",
"mobileOrder": 2,
"props": { "props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
"period": "1m" "period": "1m"
@ -35,6 +42,7 @@
}, },
{ {
"component": "addressTransactions", "component": "addressTransactions",
"mobileOrder": 3,
"props": { "props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
} }

View File

@ -10,6 +10,7 @@ let settings = [];
let configContent = {}; let configContent = {};
let gitCommitHash = ''; let gitCommitHash = '';
let packetJsonVersion = ''; let packetJsonVersion = '';
let customConfig;
try { try {
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME); const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
@ -23,7 +24,13 @@ try {
} }
} }
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html'; if (configContent && configContent.CUSTOMIZATION) {
customConfig = readConfig(configContent.CUSTOMIZATION);
}
const baseModuleName = configContent.BASE_MODULE || 'mempool';
const customBuildName = (customConfig && configContent.enterprise) ? ('.' + configContent.enterprise) : '';
const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html';
try { try {
fs.copyFileSync(indexFilePath, 'src/index.html'); fs.copyFileSync(indexFilePath, 'src/index.html');
@ -111,20 +118,14 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME); const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
let customConfigJs = ''; let customConfigJs = '';
if (configContent && configContent.CUSTOMIZATION) { if (customConfig) {
const customConfig = readConfig(configContent.CUSTOMIZATION); console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
if (customConfig) { customConfigJs = `(function (window) {
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`); window.__env = window.__env || {};
customConfigJs = `(function (window) { window.__env.customize = ${customConfig};
window.__env = window.__env || {}; }((typeof global !== 'undefined') ? global : this));
window.__env.customize = ${customConfig}; `;
}((typeof global !== 'undefined') ? global : this));
`;
} else {
throw new Error('Failed to load customization file');
}
} }
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs); writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
if (currentConfig && currentConfig === newConfig) { if (currentConfig && currentConfig === newConfig) {

View File

@ -32,10 +32,10 @@
"bootstrap": "~4.6.2", "bootstrap": "~4.6.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"cypress": "^13.8.0", "cypress": "^13.9.0",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.5.0", "echarts": "~5.5.0",
"esbuild": "^0.20.2", "esbuild": "^0.21.1",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.1.0", "ngx-echarts": "~17.1.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
@ -63,7 +63,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.8.0", "cypress": "^13.9.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",
@ -3197,9 +3197,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3212,9 +3212,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3227,9 +3227,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3242,9 +3242,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3257,9 +3257,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3272,9 +3272,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3287,9 +3287,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3302,9 +3302,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3317,9 +3317,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3332,9 +3332,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3347,9 +3347,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -3362,9 +3362,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -3377,9 +3377,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -3392,9 +3392,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3407,9 +3407,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -3422,9 +3422,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -3437,9 +3437,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3452,9 +3452,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3467,9 +3467,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3482,9 +3482,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3497,9 +3497,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3512,9 +3512,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -3527,9 +3527,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8029,9 +8029,9 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.8.0", "version": "13.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
"integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==", "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -9197,9 +9197,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
@ -9208,29 +9208,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.21.1",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.21.1",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.21.1",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.21.1",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.21.1",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.21.1",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.21.1",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.21.1",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.21.1",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.21.1",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.21.1",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.21.1",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.21.1",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.21.1",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.21.1",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.21.1",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.21.1",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.21.1",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.21.1",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/sunos-x64": "0.21.1",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-arm64": "0.21.1",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-ia32": "0.21.1",
"@esbuild/win32-x64": "0.20.2" "@esbuild/win32-x64": "0.21.1"
} }
}, },
"node_modules/esbuild-wasm": { "node_modules/esbuild-wasm": {
@ -20563,141 +20563,141 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
}, },
"@esbuild/aix-ppc64": { "@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"optional": true "optional": true
}, },
"@esbuild/android-arm": { "@esbuild/android-arm": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
"optional": true "optional": true
}, },
"@esbuild/android-arm64": { "@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"optional": true "optional": true
}, },
"@esbuild/android-x64": { "@esbuild/android-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"optional": true "optional": true
}, },
"@esbuild/darwin-arm64": { "@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"optional": true "optional": true
}, },
"@esbuild/darwin-x64": { "@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
"optional": true "optional": true
}, },
"@esbuild/freebsd-arm64": { "@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"optional": true "optional": true
}, },
"@esbuild/freebsd-x64": { "@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"optional": true "optional": true
}, },
"@esbuild/linux-arm": { "@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"optional": true "optional": true
}, },
"@esbuild/linux-arm64": { "@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"optional": true "optional": true
}, },
"@esbuild/linux-ia32": { "@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
"optional": true "optional": true
}, },
"@esbuild/linux-loong64": { "@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"optional": true "optional": true
}, },
"@esbuild/linux-mips64el": { "@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"optional": true "optional": true
}, },
"@esbuild/linux-ppc64": { "@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"optional": true "optional": true
}, },
"@esbuild/linux-riscv64": { "@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"optional": true "optional": true
}, },
"@esbuild/linux-s390x": { "@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"optional": true "optional": true
}, },
"@esbuild/linux-x64": { "@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"optional": true "optional": true
}, },
"@esbuild/netbsd-x64": { "@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"optional": true "optional": true
}, },
"@esbuild/openbsd-x64": { "@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
"optional": true "optional": true
}, },
"@esbuild/sunos-x64": { "@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"optional": true "optional": true
}, },
"@esbuild/win32-arm64": { "@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
"optional": true "optional": true
}, },
"@esbuild/win32-ia32": { "@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"optional": true "optional": true
}, },
"@esbuild/win32-x64": { "@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"optional": true "optional": true
}, },
"@eslint-community/eslint-utils": { "@eslint-community/eslint-utils": {
@ -24112,9 +24112,9 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "13.8.0", "version": "13.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
"integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==", "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.0",
@ -25032,33 +25032,33 @@
} }
}, },
"esbuild": { "esbuild": {
"version": "0.20.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"requires": { "requires": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.21.1",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.21.1",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.21.1",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.21.1",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.21.1",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.21.1",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.21.1",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.21.1",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.21.1",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.21.1",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.21.1",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.21.1",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.21.1",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.21.1",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.21.1",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.21.1",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.21.1",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.21.1",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.21.1",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/sunos-x64": "0.21.1",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-arm64": "0.21.1",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-ia32": "0.21.1",
"@esbuild/win32-x64": "0.20.2" "@esbuild/win32-x64": "0.21.1"
} }
}, },
"esbuild-wasm": { "esbuild-wasm": {

View File

@ -92,7 +92,7 @@
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"esbuild": "^0.20.2", "esbuild": "^0.21.1",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.6.0",
@ -115,7 +115,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.8.0", "cypress": "^13.9.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@ -53,6 +53,44 @@ let routes: Routes = [
}, },
] ]
}, },
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet4'
},
]
},
{ {
path: 'signet', path: 'signet',
children: [ children: [
@ -130,6 +168,10 @@ let routes: Routes = [
path: 'testnet', path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
}, },
{
path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{ {
path: 'signet', path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)

View File

@ -189,22 +189,22 @@ export const specialBlocks = {
'0': { '0': {
labelEvent: 'Genesis', labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin', labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'210000': { '210000': {
labelEvent: 'Bitcoin\'s 1st Halving', labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'420000': { '420000': {
labelEvent: 'Bitcoin\'s 2nd Halving', labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'630000': { '630000': {
labelEvent: 'Bitcoin\'s 3rd Halving', labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'709632': { '709632': {
labelEvent: 'Taproot 🌱 activation', labelEvent: 'Taproot 🌱 activation',
@ -214,62 +214,62 @@ export const specialBlocks = {
'840000': { '840000': {
labelEvent: 'Bitcoin\'s 4th Halving', labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'1050000': { '1050000': {
labelEvent: 'Bitcoin\'s 5th Halving', labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'1260000': { '1260000': {
labelEvent: 'Bitcoin\'s 6th Halving', labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'1470000': { '1470000': {
labelEvent: 'Bitcoin\'s 7th Halving', labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'1680000': { '1680000': {
labelEvent: 'Bitcoin\'s 8th Halving', labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'1890000': { '1890000': {
labelEvent: 'Bitcoin\'s 9th Halving', labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'2100000': { '2100000': {
labelEvent: 'Bitcoin\'s 10th Halving', labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'2310000': { '2310000': {
labelEvent: 'Bitcoin\'s 11th Halving', labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'2520000': { '2520000': {
labelEvent: 'Bitcoin\'s 12th Halving', labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'2730000': { '2730000': {
labelEvent: 'Bitcoin\'s 13th Halving', labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'2940000': { '2940000': {
labelEvent: 'Bitcoin\'s 14th Halving', labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
}, },
'3150000': { '3150000': {
labelEvent: 'Bitcoin\'s 15th Halving', labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'], networks: ['mainnet', 'testnet', 'testnet4'],
} }
}; };

View File

@ -266,6 +266,11 @@ const featureActivation = {
segwit: 872730, segwit: 872730,
taproot: 2032291, taproot: 2032291,
}, },
testnet4: {
rbf: 0,
segwit: 0,
taproot: 0,
},
signet: { signet: {
rbf: 0, rbf: 0,
segwit: 0, segwit: 0,

View File

@ -343,8 +343,8 @@
<a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance"> <a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance">
<img class="copa" src="/resources/profile/copa.png" /> <img class="copa" src="/resources/profile/copa.png" />
</a> </a>
<a href="https://bisq.network/" title="Bisq Network"> <a href="https://bitcoin.gob.sv" title="Oficina Nacional del Bitcoin">
<img class="bisq" src="/resources/profile/bisq.svg" /> <img class="sv" src="/resources/profile/onbtc-full.svg" />
</a> </a>
</div> </div>
</div> </div>

View File

@ -129,8 +129,9 @@
position: relative; position: relative;
width: 300px; width: 300px;
} }
.bisq { .sv {
top: 3px; height: 85px;
width: auto;
position: relative; position: relative;
} }
} }

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service';
styleUrls: ['./accelerations-list.component.scss'], styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AccelerationsListComponent implements OnInit { export class AccelerationsListComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() pending: boolean = false; @Input() pending: boolean = false;
@Input() accelerations$: Observable<Acceleration[]>; @Input() accelerations$: Observable<Acceleration[]>;
@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit {
this.accelerationList$ = this.pageSubject.pipe( this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => { switchMap((page) => {
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
}
return accelerationObservable$.pipe( return accelerationObservable$.pipe(
switchMap(response => { switchMap(response => {
let accelerations = response; let accelerations = response;
@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit {
trackByBlock(index: number, block: BlockExtended): number { trackByBlock(index: number, block: BlockExtended): number {
return block.height; return block.height;
} }
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
}
} }

View File

@ -60,7 +60,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service'; import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types'; import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils'; import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view'; import TxView from '../../block-overview-graph/tx-view';
@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended {
styleUrls: ['./accelerator-dashboard.component.scss'], styleUrls: ['./accelerator-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AcceleratorDashboardComponent implements OnInit { export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
blocks$: Observable<AccelerationBlock[]>; blocks$: Observable<AccelerationBlock[]>;
accelerations$: Observable<Acceleration[]>; accelerations$: Observable<Acceleration[]>;
pendingAccelerations$: Observable<Acceleration[]>; pendingAccelerations$: Observable<Acceleration[]>;
@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit {
firstLoad = true; firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w'; timespan: '3d' | '1w' | '1m' = '1w';
accelerationDeltaSubscription: Subscription;
graphHeight: number = 300; graphHeight: number = 300;
theme: ThemeService; theme: ThemeService;
@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.onResize(); this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.websocketService.startTrackAccelerations();
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe( this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
tap(accelerations => {
if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) {
this.audioService.playSound('bright-harmony');
}
for(const acc of accelerations) {
this.seen.add(acc.txid);
}
this.firstLoad = false;
}),
share(), share(),
); );
this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => {
if (!delta.reset) {
let hasNewAcceleration = false;
for (const acc of delta.added) {
if (!this.seen.has(acc.txid)) {
hasNewAcceleration = true;
}
this.seen.add(acc.txid);
}
for (const txid of delta.removed) {
this.seen.delete(txid);
}
if (hasNewAcceleration) {
this.audioService.playSound('bright-harmony');
}
}
});
this.accelerations$ = this.stateService.chainTip$.pipe( this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
@ -145,7 +148,7 @@ export class AcceleratorDashboardComponent implements OnInit {
} else { } else {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
return this.theme.theme === 'contrast' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1]; return this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1];
} }
} }
@ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit {
return false; return false;
} }
ngOnDestroy(): void {
this.accelerationDeltaSubscription.unsubscribe();
this.websocketService.stopTrackAccelerations();
}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {

View File

@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({ @Component({
selector: 'app-pending-stats', selector: 'app-pending-stats',
@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit {
public accelerationStats$: Observable<any>; public accelerationStats$: Observable<any>;
constructor( constructor(
private servicesApiService: ServicesApiServices, private stateService: StateService,
private websocketService: WebsocketService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe( this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe(
switchMap(accelerations => { switchMap(accelerations => {
let totalAccelerations = 0; let totalAccelerations = 0;
let totalFeeDelta = 0; let totalFeeDelta = 0;

View File

@ -1,12 +1,6 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress> <app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget"> <div [class.full-container]="!widget">
<div *ngIf="!widget" class="card-header mb-0 mb-md-2">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="address.balance-history">Balance History</span>
</div>
</div>
<ng-container *ngIf="!error"> <ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)"> (chartInit)="onChartInit($event)">

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
@ -45,23 +46,8 @@
display: flex; display: flex;
flex: 1; flex: 1;
width: 100%; width: 100%;
padding-bottom: 20px; padding-bottom: 10px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
} }
.chart-widget { .chart-widget {
width: 100%; width: 100%;

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
@ -32,7 +32,7 @@ const periodSeconds = {
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AddressGraphComponent implements OnChanges { export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() address: string; @Input() address: string;
@Input() isPubkey: boolean = false; @Input() isPubkey: boolean = false;
@Input() stats: ChainStats; @Input() stats: ChainStats;
@ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges {
data: any[] = []; data: any[] = [];
hoverData: any[] = []; hoverData: any[] = [];
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
renderer: 'svg', renderer: 'svg',
@ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges {
if (!this.address || !this.stats) { if (!this.address || !this.stats) {
return; return;
} }
(this.addressSummary$ || (this.isPubkey if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') if (this.subscription) {
: this.electrsApiService.getAddressSummary$(this.address)).pipe( this.subscription.unsubscribe();
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
)).subscribe(addressSummary => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
} }
this.isLoading = false; this.subscription = combineLatest([
this.cd.markForCheck(); this.redraw$,
}); (this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
))
]).subscribe(([redraw, addressSummary]) => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
}
this.isLoading = false;
this.cd.markForCheck();
});
} else {
// re-trigger subscription
this.redraw$.next(true);
}
} }
prepareChartOptions(summary): void { prepareChartOptions(summary): void {
if (!summary || !this.stats) {
return;
}
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
this.data = summary.map(d => { this.data = summary.map(d => {
const balance = total; const balance = total;
@ -104,8 +121,8 @@ export class AddressGraphComponent implements OnChanges {
); );
} }
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.chartOptions = { this.chartOptions = {
color: [ color: [
@ -230,6 +247,12 @@ export class AddressGraphComponent implements OnChanges {
this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
} }
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isMobile() { isMobile() {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
} }

View File

@ -3,7 +3,7 @@
} }
.qr-wrapper { .qr-wrapper {
background-color: var(--fg); background-color: #fff;
padding: 10px; padding: 10px;
padding-bottom: 5px; padding-bottom: 5px;
display: inline-block; display: inline-block;

View File

@ -53,10 +53,20 @@
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2"> <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br> <br>
<div class="title-tx">
<h2 class="text-left" i18n="address.balance-history">Balance History</h2>
</div>
<div class="box"> <div class="box">
<div class="widget-toggler" *ngIf="showBalancePeriod()">
<a href="" (click)="setBalancePeriod('all')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === 'all'}"><small i18n="all">all</small></a>
<span style="color: var(--transparent-fg); font-size: 8px"> | </span>
<a href="" (click)="setBalancePeriod('1m')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === '1m'}"><small i18n="recent">recent</small></a>
</div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" /> <app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
.qr-wrapper { .qr-wrapper {
background-color: var(--fg); background-color: #fff;
padding: 10px; padding: 10px;
padding-bottom: 5px; padding-bottom: 5px;
display: inline-block; display: inline-block;
@ -109,3 +109,19 @@ h1 {
flex-grow: 0.5; flex-grow: 0.5;
} }
} }
.widget-toggler {
font-size: 12px;
position: absolute;
top: -20px;
right: 3px;
text-align: right;
}
.toggler-option {
text-decoration: none;
}
.inactive {
color: var(--transparent-fg);
}

View File

@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy {
txCount = 0; txCount = 0;
received = 0; received = 0;
sent = 0; sent = 0;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
private tempTransactions: Transaction[]; private tempTransactions: Transaction[];
private timeTxIndexes: number[]; private timeTxIndexes: number[];
@ -175,6 +177,10 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.tempTransactions; this.transactions = this.tempTransactions;
if (this.transactions.length === this.txCount) this.fullyLoaded = true; if (this.transactions.length === this.txCount) this.fullyLoaded = true;
this.isLoadingTransactions = false; this.isLoadingTransactions = false;
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
}
}, },
(error) => { (error) => {
console.log(error); console.log(error);
@ -297,6 +303,18 @@ export class AddressComponent implements OnInit, OnDestroy {
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
} }
setBalancePeriod(period: 'all' | '1m'): boolean {
this.balancePeriod = period;
return false;
}
showBalancePeriod(): boolean {
return this.transactions?.length && (
!this.transactions[0].status?.confirmed
|| this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30))
);
}
ngOnDestroy() { ngOnDestroy() {
this.mainSubscription.unsubscribe(); this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe();

View File

@ -43,5 +43,6 @@
<ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template> <ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template> <ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'testnet4'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template> <ng-template [ngIf]="network === 'signet'">s</ng-template>
</ng-template> </ng-template>

View File

@ -1,5 +1,5 @@
.qr-wrapper { .qr-wrapper {
background-color: var(--fg); background-color: #fff;
padding: 10px; padding: 10px;
padding-bottom: 5px; padding-bottom: 5px;
display: inline-block; display: inline-block;

View File

@ -57,8 +57,9 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
calculateStats(summary: AddressTxSummary[]): void { calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0; let weekTotal = 0;
let monthTotal = 0; let monthTotal = 0;
const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30); const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) { for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
monthTotal += summary[i].value; monthTotal += summary[i].value;
if (summary[i].time >= weekAgo) { if (summary[i].time >= weekAgo) {

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
tooltipPosition: Position; tooltipPosition: Position;
readyNextFrame = false; readyNextFrame = false;
lastUpdate: number = 0;
pendingUpdate: {
count: number,
add: { [txid: string]: TransactionStripped },
remove: { [txid: string]: string },
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
direction?: string,
} = {
count: 0,
add: {},
remove: {},
change: {},
direction: 'left',
};
searchText: string; searchText: string;
searchSubscription: Subscription; searchSubscription: Subscription;
@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
destroy(): void { destroy(): void {
if (this.scene) { if (this.scene) {
this.scene.destroy(); this.scene.destroy();
this.clearUpdateQueue();
this.start(); this.start();
} }
} }
@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
this.filtersAvailable = filtersAvailable; this.filtersAvailable = filtersAvailable;
if (this.scene) { if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions); this.scene.setup(transactions);
this.readyNextFrame = true; this.readyNextFrame = true;
this.start(); this.start();
@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
enter(transactions: TransactionStripped[], direction: string): void { enter(transactions: TransactionStripped[], direction: string): void {
if (this.scene) { if (this.scene) {
this.clearUpdateQueue();
this.scene.enter(transactions, direction); this.scene.enter(transactions, direction);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
exit(direction: string): void { exit(direction: string): void {
if (this.scene) { if (this.scene) {
this.clearUpdateQueue();
this.scene.exit(direction); this.scene.exit(direction);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
@ -213,13 +231,67 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) { if (this.scene) {
this.clearUpdateQueue();
this.scene.replace(transactions || [], direction, sort, startTime); this.scene.replace(transactions || [], direction, sort, startTime);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
} }
} }
// collates deferred updates into a set of consistent pending changes
queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
for (const tx of add) {
this.pendingUpdate.add[tx.txid] = tx;
delete this.pendingUpdate.remove[tx.txid];
delete this.pendingUpdate.change[tx.txid];
}
for (const txid of remove) {
delete this.pendingUpdate.add[txid];
this.pendingUpdate.remove[txid] = txid;
delete this.pendingUpdate.change[txid];
}
for (const tx of change) {
if (this.pendingUpdate.add[tx.txid]) {
this.pendingUpdate.add[tx.txid].rate = tx.rate;
this.pendingUpdate.add[tx.txid].acc = tx.acc;
} else {
this.pendingUpdate.change[tx.txid] = tx;
}
}
this.pendingUpdate.direction = direction;
this.pendingUpdate.count++;
}
deferredUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
this.queueUpdate(add, remove, change, direction);
this.applyQueuedUpdates();
}
applyQueuedUpdates(): void {
if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), this.pendingUpdate.direction);
this.clearUpdateQueue();
}
}
clearUpdateQueue(): void {
this.pendingUpdate = {
count: 0,
add: {},
remove: {},
change: {},
};
this.lastUpdate = performance.now();
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
// merge any pending changes into this update
this.queueUpdate(add, remove, change);
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout);
this.clearUpdateQueue();
}
applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) { if (this.scene) {
add = add.filter(tx => !this.scene.txs[tx.txid]); add = add.filter(tx => !this.scene.txs[tx.txid]);
remove = remove.filter(txid => this.scene.txs[txid]); remove = remove.filter(txid => this.scene.txs[txid]);
@ -230,6 +302,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, change, direction, resetLayout);
this.start(); this.start();
this.lastUpdate = performance.now();
this.updateSearchHighlight(); this.updateSearchHighlight();
} }
} }
@ -370,6 +443,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (!now) { if (!now) {
now = performance.now(); now = performance.now();
} }
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene // skip re-render if there's no change to the scene
if (this.scene && this.gl) { if (this.scene && this.gl) {
/* SET UP SHADER UNIFORMS */ /* SET UP SHADER UNIFORMS */
@ -577,13 +651,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) { getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => { return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
if (this.themeService.theme !== 'contrast') { if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)); return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else { } else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)); return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
} }
} else { } else {
if (this.themeService.theme !== 'contrast') { if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction( return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx, tx,
defaultColors.unmatchedfee, defaultColors.unmatchedfee,

View File

@ -13,7 +13,7 @@ export default class BlockScene {
theme: ThemeService; theme: ThemeService;
orientation: string; orientation: string;
flip: boolean; flip: boolean;
animationDuration: number = 900; animationDuration: number = 1000;
configAnimationOffset: number | null; configAnimationOffset: number | null;
animationOffset: number; animationOffset: number;
highlightingEnabled: boolean; highlightingEnabled: boolean;
@ -69,7 +69,7 @@ export default class BlockScene {
} }
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
this.theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.updateAllColors(); this.updateAllColors();
} }
@ -179,7 +179,7 @@ export default class BlockScene {
removed.forEach(tx => { removed.forEach(tx => {
tx.destroy(); tx.destroy();
}); });
}, 1000); }, (startTime - performance.now()) + this.animationDuration + 1000);
if (resetLayout) { if (resetLayout) {
add.forEach(tx => { add.forEach(tx => {
@ -239,14 +239,14 @@ export default class BlockScene {
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void { ): void {
this.animationDuration = animationDuration || 1000; this.animationDuration = animationDuration || this.animationDuration || 1000;
this.configAnimationOffset = animationOffset; this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset; this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation; this.orientation = orientation;
this.flip = flip; this.flip = flip;
this.vertexArray = vertexArray; this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting; this.highlightingEnabled = highlighting;
theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction; theme.theme === 'contrast' || theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.theme = theme; this.theme = theme;
this.scene = { this.scene = {

View File

@ -177,7 +177,7 @@ export function ageColorFunction(
return auditColors.accelerated; return auditColors.accelerated;
} }
const color = theme !== 'contrast' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime); const color = theme !== 'contrast' && theme !== 'bukele' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime);
const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60))))))); const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60)))))));
return { return {

View File

@ -1,6 +1,6 @@
.block-overview-tooltip { .block-overview-tooltip {
position: absolute; position: absolute;
background: rgba(#11131f, 0.95); background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5); box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey); color: var(--tooltip-grey);
@ -30,7 +30,7 @@ th, td {
} }
.badge.badge-accelerated { .badge.badge-accelerated {
background-color: var(--tertiary); background-color: #653b9c;
box-shadow: #ad7de57f 0px 0px 12px -2px; box-shadow: #ad7de57f 0px 0px 12px -2px;
color: white; color: white;
animation: acceleratePulse 1s infinite; animation: acceleratePulse 1s infinite;
@ -71,7 +71,7 @@ th, td {
} }
@keyframes acceleratePulse { @keyframes acceleratePulse {
0% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; } 0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;} 50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
100% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; } 100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
} }

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -136,7 +136,12 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
return of(transactions); return of(transactions);
}) })
), ),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) : of([]) this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
: of([])
]); ]);
} }
), ),

View File

@ -345,7 +345,12 @@ export class BlockComponent implements OnInit, OnDestroy {
return of(null); return of(null);
}) })
), ),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) : of([]) this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
: of([])
]); ]);
}) })
) )

View File

@ -63,7 +63,7 @@
.fee-span { .fee-span {
font-size: 11px; font-size: 11px;
margin-bottom: 5px; margin-bottom: 5px;
color: #fff000; color: var(--yellow);
} }
.transaction-count { .transaction-count {
@ -130,7 +130,7 @@
height: 0; height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent; border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent; border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid #FFF; border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
} }
.flashing { .flashing {

View File

@ -70,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
liquid: ['var(--liquid)', 'var(--testnet-alt)'], liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'], 'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['var(--testnet)', 'var(--testnet-alt)'], testnet: ['var(--testnet)', 'var(--testnet-alt)'],
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'], signet: ['var(--signet)', 'var(--signet-alt)'],
}; };
@ -349,7 +350,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return { return {
left: addLeft + this.blockOffset * index + 'px', left: addLeft + this.blockOffset * index + 'px',
background: `repeating-linear-gradient( background: `repeating-linear-gradient(
#2d3348, var(--secondary),
var(--secondary) ${greenBackgroundHeight}%, var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[this.network][1]} 100% ${this.gradientColors[this.network][1]} 100%
@ -361,7 +362,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
convertStyleForLoadingBlock(style) { convertStyleForLoadingBlock(style) {
return { return {
...style, ...style,
background: "#2d3348", background: "var(--secondary)",
}; };
} }
@ -370,7 +371,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return { return {
left: addLeft + (this.blockOffset * index) + 'px', left: addLeft + (this.blockOffset * index) + 'px',
background: "#2d3348", background: "var(--secondary)",
}; };
} }

View File

@ -54,7 +54,7 @@
} }
.time-toggle { .time-toggle {
color: white; color: var(--fg);
font-size: 0.8rem; font-size: 0.8rem;
position: absolute; position: absolute;
bottom: -1.8em; bottom: -1.8em;
@ -68,7 +68,7 @@
} }
.block-display-toggle { .block-display-toggle {
color: white; color: var(--fg);
font-size: 0.8rem; font-size: 0.8rem;
position: absolute; position: absolute;
bottom: 15.8em; bottom: 15.8em;

View File

@ -55,7 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
firstValueFrom(this.stateService.chainTip$).then(() => { firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false; this.loadingTip = false;
}); });
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'size'; this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees';
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@ -32,11 +32,12 @@ export class ClockComponent implements OnInit {
limitHeight: number; limitHeight: number;
gradientColors = { gradientColors = {
'': ['#9339f4', '#105fb0'], '': ['var(--mainnet-alt)', 'var(--primary)'],
liquid: ['#116761', '#183550'], liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['#494a4a', '#272e46'], 'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['#1d486f', '#183550'], testnet: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['#6f1d5d', '#471850'], testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'],
}; };
constructor( constructor(
@ -99,8 +100,8 @@ export class ClockComponent implements OnInit {
return { return {
background: `repeating-linear-gradient( background: `repeating-linear-gradient(
#2d3348, var(--secondary),
#2d3348 ${greenBackgroundHeight}%, var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%, ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[''][1]} 100% ${this.gradientColors[''][1]} 100%
)`, )`,

View File

@ -4,7 +4,7 @@
@for (widget of widgets; track widget.component) { @for (widget of widgets; track widget.component) {
@switch (widget.component) { @switch (widget.component) {
@case ('fees') { @case ('fees') {
<div class="col card-wrapper"> <div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div> <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card"> <div class="card">
<div class="card-body less-padding"> <div class="card-body less-padding">
@ -14,12 +14,12 @@
</div> </div>
} }
@case ('difficulty') { @case ('difficulty') {
<div class="col"> <div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<app-difficulty></app-difficulty> <app-difficulty></app-difficulty>
</div> </div>
} }
@case ('goggles') { @case ('goggles') {
<div class="col"> <div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]"> <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
@ -48,7 +48,7 @@
</div> </div>
} }
@case ('incoming') { @case ('incoming') {
<div class="col"> <div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body"> <div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
@ -93,7 +93,7 @@
</ng-template> </ng-template>
} }
@case ('replacements') { @case ('replacements') {
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
@ -140,7 +140,7 @@
</ng-template> </ng-template>
} }
@case ('blocks') { @case ('blocks') {
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
@ -184,7 +184,7 @@
</ng-template> </ng-template>
} }
@case ('transactions') { @case ('transactions') {
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5> <h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
@ -224,13 +224,13 @@
</ng-template> </ng-template>
} }
@case ('balance') { @case ('balance') {
<div class="col card-wrapper"> <div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="dashboard.treasury">Treasury</div> <div class="main-title" i18n="dashboard.treasury">Treasury</div>
<app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget> <app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget>
</div> </div>
} }
@case ('address') { @case ('address') {
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]"> <a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
@ -238,13 +238,13 @@
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a> </a>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address?.chain_stats" [widget]="true" [height]="graphHeight"></app-address-graph> <app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph>
</div> </div>
</div> </div>
</div> </div>
} }
@case ('addressTransactions') { @case ('addressTransactions') {
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]"> <a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
@ -257,6 +257,22 @@
</div> </div>
</div> </div>
} }
@case ('twitter') {
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2 d-flex flex-column">
<a class="title-link" [href]="'https://x.com/' + widget.props?.handle" target="_blank">
<h5 class="card-title d-inline" i18n="dashboard.x-timeline">X Timeline</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
@defer {
<app-twitter-widget [handle]="widget.props?.handle" style="flex-grow: 1"></app-twitter-widget>
}
</div>
</div>
</div>
}
} }
} }
</div> </div>

View File

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface'; import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
@ -57,6 +57,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
incomingGraphHeight: number = 300; incomingGraphHeight: number = 300;
graphHeight: number = 300; graphHeight: number = 300;
webGlEnabled = true; webGlEnabled = true;
isMobile: boolean = window.innerWidth <= 767.98;
widgets; widgets;
@ -85,6 +86,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private seoService: SeoService, private seoService: SeoService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object, @Inject(PLATFORM_ID) private platformId: Object,
) { ) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
@ -230,8 +232,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.stateService.live2Chart$ this.stateService.live2Chart$
.pipe( .pipe(
scan((acc, stats) => { scan((acc, stats) => {
const now = Date.now() / 1000;
const start = now - (2 * 60 * 60);
acc.unshift(stats); acc.unshift(stats);
acc = acc.slice(0, 120); acc = acc.filter(p => p.added >= start);
return acc; return acc;
}, (mempoolStats || [])) }, (mempoolStats || []))
), ),
@ -283,8 +287,8 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
startAddressSubscription(): void { startAddressSubscription(): void {
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) { if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address; let addressString = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address; addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(addressString)) ? addressString.toLowerCase() : addressString;
this.addressSubscription = ( this.addressSubscription = (
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
@ -299,6 +303,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
).subscribe((address: Address) => { ).subscribe((address: Address) => {
this.websocketService.startTrackAddress(address.address); this.websocketService.startTrackAddress(address.address);
this.address = address; this.address = address;
this.cd.markForCheck();
}); });
this.addressSummary$ = ( this.addressSummary$ = (
@ -368,5 +373,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.goggleResolution = 86; this.goggleResolution = 86;
this.graphHeight = 310; this.graphHeight = 310;
} }
this.isMobile = window.innerWidth <= 767.98;
} }
} }

View File

@ -119,7 +119,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,9 +1,9 @@
.difficulty-tooltip { .difficulty-tooltip {
position: fixed; position: fixed;
background: rgba(#11131f, 0.95); background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5); box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1; color: var(--tooltip-grey);
padding: 10px 15px; padding: 10px 15px;
text-align: left; text-align: left;
pointer-events: none; pointer-events: none;

View File

@ -15,8 +15,8 @@
<svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none"> <svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs> <defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> <linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" /> <stop offset="0%" stop-color="var(--primary)" />
<stop offset="100%" stop-color="#9339f4" /> <stop offset="100%" stop-color="var(--mainnet-alt)" />
</linearGradient> </linearGradient>
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> <linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" /> <stop offset="0%" stop-color="#2486eb" />

View File

@ -128,7 +128,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
@ -223,7 +224,7 @@
height: 100%; height: 100%;
} }
.background { .background {
background: linear-gradient(to right, var(--primary), #9339f4); background: linear-gradient(to right, var(--primary), var(--mainnet-alt));
left: 0; left: 0;
right: 0; right: 0;
} }

View File

@ -79,7 +79,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
transition: background-color 1s; transition: background-color 1s;
color: var(--color-fg); color: #fff;
&.priority { &.priority {
@media (767px < width < 992px), (width < 576px) { @media (767px < width < 992px), (width < 576px) {
width: 100%; width: 100%;

View File

@ -16,8 +16,8 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
isLoading$: Observable<boolean>; isLoading$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>; recommendedFees$: Observable<Recommendedfees>;
themeSubscription: Subscription; themeSubscription: Subscription;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)'; gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))';
noPriority = '#2e324e'; noPriority = 'var(--skeleton-bg)';
fees: Recommendedfees; fees: Recommendedfees;
constructor( constructor(

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -11,7 +11,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -66,7 +66,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
if (!this.data) { if (!this.data) {
return; return;
} }
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference'); this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8)); const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize); this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) { if (this.outlierCappingEnabled === true) {
@ -216,22 +216,19 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
type: 'line', type: 'line',
}, },
formatter: (params: any) => { formatter: (params: any) => {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue); const bestItem = params.reduce((best, item) => {
return (item.seriesName === 'data' && (!best || best.value[1] < item.value[1])) ? item : best;
}, null);
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, bestItem.axisValue);
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`; const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>'; let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
params.map((item: any, index: number) => { if (bestItem) {
itemFormatted += `<div class="item">
//Do no include MA in tooltip legend! <div class="indicator-container">${colorSpan(bestItem.color)}</div>
if (item.seriesName !== 'MA') {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div> <div class="grow"></div>
<div class="value">${formatNumber(item.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div> <div class="value">${formatNumber(bestItem.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
</div>`; </div>`;
} }
}
});
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}" return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`; style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
} }

View File

@ -51,7 +51,8 @@
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a> <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a> <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6> <h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a> <a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a> <a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>

View File

@ -4,6 +4,7 @@
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 100; z-index: 100;
background-color: var(--bg);
} }
li.nav-item.active { li.nav-item.active {
@ -17,7 +18,7 @@ fa-icon {
.navbar { .navbar {
z-index: 100; z-index: 100;
min-height: 64px; min-height: 64px;
background-color: var(--bg); background-color: var(--nav-bg);
} }
li.nav-item { li.nav-item {
@ -48,7 +49,7 @@ li.nav-item {
} }
.navbar-nav { .navbar-nav {
background: var(--navbar-bg); background: var(--nav-bg);
bottom: 0; bottom: 0;
box-shadow: 0px 0px 15px 0px #000; box-shadow: 0px 0px 15px 0px #000;
flex-direction: row; flex-direction: row;
@ -169,4 +170,8 @@ nav {
margin-left: 5px; margin-left: 5px;
margin-right: 0px; margin-right: 0px;
} }
}
.beta-network {
font-size: 8px;
} }

View File

@ -6,7 +6,7 @@
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
} }
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> <img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3">
} @else { } @else {
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
@ -15,7 +15,8 @@
<div [ngSwitch]="network.val"> <div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span> <span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span> <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet3</span>
<span *ngSwitchCase="'testnet4'" class="network testnet"><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet4</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span> <span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span> <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span> <span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>

View File

@ -5,6 +5,7 @@
max-width: 1200px; max-width: 1200px;
max-height: 600px; max-height: 600px;
padding-top: 80px; padding-top: 80px;
background: var(--nav-bg);
header { header {
position: absolute; position: absolute;
@ -18,7 +19,7 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: var(--stat-box-bg); background: var(--nav-bg);
text-align: start; text-align: start;
font-size: 1.8em; font-size: 1.8em;
} }

View File

@ -17,16 +17,16 @@
<!-- Large screen --> <!-- Large screen -->
<a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@if (enterpriseInfo?.header_img) { @if (enterpriseInfo?.header_img) {
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="48px" class="mr-3">
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
} }
@ -38,34 +38,39 @@
</a> </a>
<!-- Mobile --> <!-- Mobile -->
<a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain && enterpriseInfo"> @if (enterpriseInfo?.header_img) {
<div class="subdomain_container"> <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> } @else {
</div> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="vertical-line"></div> <div class="subdomain_container">
</ng-template> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> </div>
@if (enterpriseInfo?.header_img) { <div class="vertical-line"></div>
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px"> </ng-template>
} @else { <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images> @if (enterpriseInfo?.header_img) {
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
} } @else {
<div class="connection-badge"> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div> }
</div> <div class="connection-badge">
</ng-container> <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
</div>
</ng-container>
}
</a> </a>
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED"> <div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true"> <button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images> <app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
</button> </button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> <a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a> <a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a> <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<h6 *ngIf="env.LIQUID_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6> <h6 *ngIf="env.LIQUID_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a> <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a> <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
@ -87,7 +92,7 @@
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a> <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li> </li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-lightning" *ngIf="stateService.env.LIGHTNING && lightningNetworks.includes(stateService.network)">
<a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon> <a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon>
</a> </a>
</li> </li>
@ -114,7 +119,7 @@
<div class="empty-sidenav"><!-- empty sidenav needed to push footer down the screen --></div> <div class="empty-sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column"> <div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert> <app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'testnet4' || network.val === 'signet'"></app-testnet-alert>
<main style="min-width: 375px; max-width: 100vw"> <main style="min-width: 375px; max-width: 100vw">
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -3,6 +3,7 @@
position: -webkit-sticky; position: -webkit-sticky;
top: 0; top: 0;
width: 100%; width: 100%;
background-color: var(--bg);
z-index: 100; z-index: 100;
} }
@ -18,7 +19,7 @@ fa-icon {
z-index: 100; z-index: 100;
min-height: 64px; min-height: 64px;
width: 100%; width: 100%;
background-color: var(--bg); background-color: var(--nav-bg);
} }
li.nav-item { li.nav-item {
@ -59,7 +60,7 @@ li.nav-item {
} }
.navbar-nav { .navbar-nav {
background: var(--navbar-bg); background: var(--nav-bg);
bottom: 0; bottom: 0;
box-shadow: 0px 0px 15px 0px #000; box-shadow: 0px 0px 15px 0px #000;
flex-direction: row; flex-direction: row;
@ -243,6 +244,10 @@ nav {
} }
} }
.beta-network {
font-size: 8px;
}
.current-network-svg { .current-network-svg {
width: 20px; width: 20px;
height: 20px; height: 20px;

View File

@ -27,6 +27,7 @@ export class MasterPageComponent implements OnInit, OnDestroy {
subdomain = ''; subdomain = '';
networkPaths: { [network: string]: string }; networkPaths: { [network: string]: string };
networkPaths$: Observable<Record<string, string>>; networkPaths$: Observable<Record<string, string>>;
lightningNetworks = ['', 'mainnet', 'bitcoin', 'testnet', 'signet'];
footerVisible = true; footerVisible = true;
user: any = undefined; user: any = undefined;
servicesEnabled = false; servicesEnabled = false;

View File

@ -1,11 +1,10 @@
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, import { Component, ViewChild, Input, Output, EventEmitter,
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { MempoolBlockDelta } from '../../interfaces/websocket.interface'; import { MempoolBlockDelta, isMempoolDelta } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface'; import { TransactionStripped } from '../../interfaces/node-api.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs'; import { Subscription, BehaviorSubject } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -39,10 +38,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
poolDirection: string = 'left'; poolDirection: string = 'left';
blockSub: Subscription; blockSub: Subscription;
rateLimit = 1000;
private lastEventTime = Date.now() - this.rateLimit;
private subId = 0;
firstLoad: boolean = true; firstLoad: boolean = true;
constructor( constructor(
@ -62,39 +57,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.blockSub = merge( this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
this.stateService.mempoolBlockTransactions$,
this.stateService.mempoolBlockDelta$,
).pipe(
concatMap(update => {
const now = Date.now();
const timeSinceLastEvent = now - this.lastEventTime;
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
const subId = this.subId;
// If time since last event is less than X seconds, delay this event
if (timeSinceLastEvent < this.rateLimit) {
return timer(this.rateLimit - timeSinceLastEvent).pipe(
// Emit the event after the timer
map(() => ({ update, subId }))
);
} else {
// If enough time has passed, emit the event immediately
return of({ update, subId });
}
})
).subscribe(({ update, subId }) => {
// discard stale updates after a block transition
if (subId !== this.subId) {
return;
}
// process update // process update
if (update['added']) { if (isMempoolDelta(update)) {
// delta // delta
this.updateBlock(update as MempoolBlockDelta); this.updateBlock(update);
} else { } else {
const transactionsStripped = update as TransactionStripped[]; const transactionsStripped = update.transactions;
// new transactions // new transactions
if (this.firstLoad) { if (this.firstLoad) {
this.replaceBlock(transactionsStripped); this.replaceBlock(transactionsStripped);
@ -137,7 +106,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngOnChanges(changes): void { ngOnChanges(changes): void {
if (changes.index) { if (changes.index) {
this.subId++;
this.firstLoad = true; this.firstLoad = true;
if (this.blockGraph) { if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
@ -173,7 +141,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
this.blockGraph.replace(delta.added, direction); this.blockGraph.replace(delta.added, direction);
} else { } else {
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); if (blockMined) {
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
} else {
this.blockGraph.deferredUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection);
}
} }
this.lastBlockHeight = this.stateService.latestBlockHeight; this.lastBlockHeight = this.stateService.latestBlockHeight;

View File

@ -56,7 +56,7 @@
.fee-span { .fee-span {
font-size: 11px; font-size: 11px;
margin-bottom: 5px; margin-bottom: 5px;
color: #fff000; color: var(--yellow);
} }
.transaction-count { .transaction-count {
@ -119,7 +119,7 @@
height: 0; height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent; border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent; border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid #FFF; border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
} }
.blockLink { .blockLink {

View File

@ -77,7 +77,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
} }
this.isWidget = this.template === 'widget'; this.isWidget = this.template === 'widget';
this.showCount = !this.isWidget && !this.hideCount; this.showCount = !this.isWidget && !this.hideCount;
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference'); this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([])); this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart(); this.mountFeeChart();
} }
@ -256,11 +256,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const itemFormatted = []; const itemFormatted = [];
let sum = 0; let sum = 0;
let progressPercentageText = ''; let progressPercentageText = '';
let countItem; const unfilteredItems = this.inverted ? [...params].reverse() : params;
let items = this.inverted ? [...params].reverse() : params; const countItem = unfilteredItems.find(p => p.seriesName === 'count');
if (items[items.length - 1].seriesName === 'count') { const usedSeries = {};
countItem = items.pop(); const items = unfilteredItems.filter(p => {
} if (usedSeries[p.seriesName] || p.seriesName === 'count') {
return false;
}
usedSeries[p.seriesName] = true;
return true;
});
items.map((item: any, index: number) => { items.map((item: any, index: number) => {
sum += item.value[1]; sum += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100; const progressPercentage = (item.value[1] / totalValue) * 100;

View File

@ -63,7 +63,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -1,10 +1,10 @@
.rbf-tooltip { .rbf-tooltip {
position: fixed; position: fixed;
z-index: 3; z-index: 3;
background: rgba(#11131f, 0.95); background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5); box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1; color: var(--tooltip-grey);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;

View File

@ -159,7 +159,7 @@
&.selected { &.selected {
.shape-border { .shape-border {
background: #9339f4; background: var(--mainnet-alt);
} }
} }
@ -183,7 +183,7 @@
width: calc(1em + 16px); width: calc(1em + 16px);
.shape { .shape {
border: solid 4px #9339f4; border: solid 4px var(--mainnet-alt);
} }
&:hover { &:hover {

View File

@ -179,7 +179,7 @@ export class SearchFormComponent implements OnInit {
const lightningResults = result[1]; const lightningResults = result[1];
// Do not show date and timestamp results for liquid // Do not show date and timestamp results for liquid
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet'; const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet';
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin; const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;

View File

@ -60,6 +60,9 @@
<ng-container *ngSwitchCase="'testnet'"> <ng-container *ngSwitchCase="'testnet'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component> <ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'testnet4'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
</ng-container>
<ng-container *ngSwitchCase="'liquid'"> <ng-container *ngSwitchCase="'liquid'">
<ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#2cccbf', color2: '#9ef2ed'}"></ng-component> <ng-component *ngTemplateOutlet="liquidLogo; context: {$implicit: '', width, height, viewBox, color1: '#2cccbf', color2: '#9ef2ed'}"></ng-component>
</ng-container> </ng-container>

View File

@ -71,7 +71,9 @@ export class TelevisionComponent implements OnInit, OnDestroy {
mempoolStats = newStats; mempoolStats = newStats;
} else if (['2h', '24h'].includes(this.fragment)) { } else if (['2h', '24h'].includes(this.fragment)) {
mempoolStats.unshift(newStats[0]); mempoolStats.unshift(newStats[0]);
mempoolStats = mempoolStats.slice(0, mempoolStats.length - 1); const now = Date.now() / 1000;
const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) );
mempoolStats = mempoolStats.filter(p => p.added >= start);
} }
return mempoolStats; return mempoolStats;
}) })

View File

@ -11,7 +11,7 @@
<div class="text-left"> <div class="text-left">
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p> <p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p> <p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>

View File

@ -0,0 +1,53 @@
<div class="container-xl">
<h1 class="text-left" i18n="shared.test-transactions|Test Transactions">Test Transactions</h1>
<form [formGroup]="testTxsForm" (submit)="testTxsForm.valid && testTxs()" novalidate>
<label for="maxfeerate" i18n="test.tx.raw-hex">Raw hex</label>
<div class="mb-3">
<textarea formControlName="txs" class="form-control" rows="5" i18n-placeholder="transaction.test-transactions" placeholder="Comma-separated list of raw transactions"></textarea>
</div>
<label for="maxfeerate" i18n="test.tx.max-fee-rate">Maximum fee rate (sat/vB)</label>
<input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate"
[value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}">
<br>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.test-transactions|Test Transactions">Test Transactions</button>
<p class="red-color d-inline">{{ error }}</p>
</form>
<br>
<div class="box" *ngIf="results?.length">
<table class="accept-results table table-fixed table-borderless table-striped">
<tbody>
<tr>
<th class="allowed" i18n="test-tx.is-allowed">Allowed?</th>
<th class="txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="rate" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</th>
<th class="reason" i18n="test-tx.rejection-reason">Rejection reason</th>
</tr>
<ng-container *ngFor="let result of results;">
<tr>
<td class="allowed">
<ng-container [ngSwitch]="result.allowed">
<span *ngSwitchCase="true"></span>
<span *ngSwitchCase="false"></span>
<span *ngSwitchDefault>-</span>
</ng-container>
</td>
<td class="txid">
<app-truncate [text]="result.txid || '-'"></app-truncate>
</td>
<td class="rate">
<app-fee-rate *ngIf="result.fees?.['effective-feerate'] != null" [fee]="result.fees?.['effective-feerate'] * 100000"></app-fee-rate>
<span *ngIf="result.fees?.['effective-feerate'] == null">-</span>
</td>
<td class="reason">
{{ result['reject-reason'] || '-' }}
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,34 @@
.accept-results {
td, th {
&.allowed {
width: 10%;
text-align: center;
}
&.txid {
width: 50%;
}
&.rate {
width: 20%;
text-align: right;
white-space: wrap;
}
&.reason {
width: 20%;
text-align: right;
white-space: wrap;
}
}
@media (max-width: 950px) {
table-layout: auto;
td, th {
&.allowed {
width: 100px;
}
&.txid {
max-width: 200px;
}
}
}
}

View File

@ -0,0 +1,86 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-test-transactions',
templateUrl: './test-transactions.component.html',
styleUrls: ['./test-transactions.component.scss']
})
export class TestTransactionsComponent implements OnInit {
testTxsForm: UntypedFormGroup;
error: string = '';
results: TestMempoolAcceptResult[] = [];
isLoading = false;
invalidMaxfeerate = false;
constructor(
private formBuilder: UntypedFormBuilder,
private apiService: ApiService,
public stateService: StateService,
private seoService: SeoService,
private ogService: OpenGraphService,
) { }
ngOnInit(): void {
this.testTxsForm = this.formBuilder.group({
txs: ['', Validators.required],
maxfeerate: ['', Validators.min(0)]
});
this.seoService.setTitle($localize`:@@meta.title.test-txs:Test Transactions`);
this.ogService.setManualOgImage('tx-push.jpg');
}
testTxs() {
let txs: string[] = [];
try {
txs = (this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim());
if (!txs?.length) {
this.error = 'At least one transaction is required';
return;
} else if (txs.length > 25) {
this.error = 'Exceeded maximum of 25 transactions';
return;
}
} catch (e) {
this.error = e?.message;
return;
}
let maxfeerate;
this.invalidMaxfeerate = false;
try {
const maxfeerateVal = this.testTxsForm.get('maxfeerate')?.value;
if (maxfeerateVal != null && maxfeerateVal !== '') {
maxfeerate = parseFloat(maxfeerateVal) / 100_000;
}
} catch (e) {
this.invalidMaxfeerate = true;
}
this.isLoading = true;
this.error = '';
this.results = [];
this.apiService.testTransactions$(txs, maxfeerate === 0.1 ? null : maxfeerate)
.subscribe((result) => {
this.isLoading = false;
this.results = result || [];
this.testTxsForm.reset();
},
(error) => {
if (typeof error.error === 'string') {
const matchText = error.error.match('"message":"(.*?)"');
this.error = matchText && matchText[1] || error.error;
} else if (error.message) {
this.error = error.message;
}
this.isLoading = false;
});
}
}

View File

@ -11,7 +11,7 @@ import { Subscription } from 'rxjs';
}) })
export class ThemeSelectorComponent implements OnInit { export class ThemeSelectorComponent implements OnInit {
themeForm: UntypedFormGroup; themeForm: UntypedFormGroup;
themes = ['default', 'contrast', 'wiz']; themes = ['default', 'contrast', 'wiz', 'bukele'];
themeSubscription: Subscription; themeSubscription: Subscription;
constructor( constructor(

View File

@ -1,15 +1,34 @@
<div class="mobile-wrapper"> <div class="mobile-wrapper">
<div class="mobile-container"> <div class="mobile-container">
<div class="panel"> <div class="panel">
<div class="field nav-header"> <div class="nav-header">
<app-svg-images name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> @if (enterpriseInfo?.header_img) {
<div [ngSwitch]="network" class="network-label"> <a class="d-flex" [routerLink]="['/' | relativeUrl]">
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
</a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</a>
<div class="vertical-line"></div>
}
@if (!enterpriseInfo?.header_img) {
<a [routerLink]="['/' | relativeUrl]">
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" style="width: 144px; height: 36px" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 144px; height: 36px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
</a>
}
@if (enterpriseInfo?.header_img || (!enterpriseInfo?.img && !enterpriseInfo?.imageMd5)) {
<div [ngSwitch]="network" class="network-label" [class.hide-name]="enterpriseInfo?.header_img">
<span *ngSwitchCase="'signet'" class="network signet"><span class="name">Bitcoin Signet</span><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> <span *ngSwitchCase="'signet'" class="network signet"><span class="name">Bitcoin Signet</span><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> <span *ngSwitchCase="'testnet'" class="network testnet"><span class="name">Bitcoin Testnet3</span><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'testnet4'" class="network testnet"><span class="name">Bitcoin Testnet4</span><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'liquid'" class="network liquid"><span class="name">Liquid</span><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> <span *ngSwitchCase="'liquid'" class="network liquid"><span class="name">Liquid</span><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><span class="name">Liquid Testnet</span><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><span class="name">Liquid Testnet</span><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
<span *ngSwitchDefault class="network mainnet"><span class="name">Bitcoin</span><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span> <span *ngSwitchDefault class="network mainnet"><span class="name">Bitcoin</span><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="display: inline-block" class="mainnet ml-2"></app-svg-images></span>
</div> </div>
}
</div> </div>
<div class="field"> <div class="field">
<div class="label" i18n="shared.transaction">Transaction</div> <div class="label" i18n="shared.transaction">Transaction</div>

View File

@ -40,7 +40,14 @@
} }
.nav-header { .nav-header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
max-width: 100%;
padding: 1em;
position: relative; position: relative;
background: var(--nav-bg);
box-shadow: 0 -5px 15px #000; box-shadow: 0 -5px 15px #000;
z-index: 100; z-index: 100;
align-items: center; align-items: center;
@ -53,6 +60,40 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
&.hide-name .name {
display: none;
}
}
.subdomain_logo {
height: 35px;
overflow: clip;
max-width: 140px;
margin: auto;
align-self: center;
.rounded {
border-radius: 5px;
}
}
.subdomain_container {
max-width: 140px;
text-align: center;
align-self: center;
}
.vertical-line {
border-left: 1px solid #444;
height: 30px;
margin-left: 10px;
margin-right: 10px;
margin-top: 3px;
}
.logo-holder {
display: flex;
flex-direction: row;
} }
} }

View File

@ -113,6 +113,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
scrollIntoAccelPreview = false; scrollIntoAccelPreview = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
enterpriseInfo: any;
enterpriseInfo$: Subscription;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
@ -152,6 +156,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.enterpriseService.page(); this.enterpriseService.page();
this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => {
this.enterpriseInfo = info;
});
this.websocketService.want(['blocks', 'mempool-blocks']); this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe( this.stateService.networkChanged$.subscribe(
(network) => { (network) => {
@ -702,6 +710,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
this.miningSubscription?.unsubscribe(); this.miningSubscription?.unsubscribe();
this.currencyChangeSubscription?.unsubscribe(); this.currencyChangeSubscription?.unsubscribe();
this.enterpriseInfo$?.unsubscribe();
this.leaveTransaction(); this.leaveTransaction();
} }
} }

View File

@ -103,7 +103,8 @@ td.amount.large {
margin-top: 10px; margin-top: 10px;
} }
.assetBox { .assetBox {
background-color: #653b9c90; background: color-mix(in srgb, var(--tertiary) 56%, transparent);
} }
.details-container { .details-container {

View File

@ -0,0 +1,16 @@
@if (loading) {
<div class="spinner-wrapper">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} @else if (error) {
<div class="error-wrapper">
<span>failed to load X timeline</span>
</div>
}
<iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true"
title="Twitter Timeline"
[src]="iframeSrc"
style="position: static; visibility: visible; width: 100%; height: 100%; display: block; flex-grow: 1;"
(load)="onReady()"
></iframe>

View File

@ -0,0 +1,10 @@
.spinner-wrapper, .error-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,71 @@
import { Component, Input, ChangeDetectionStrategy, SecurityContext, SimpleChanges, OnChanges } from '@angular/core';
import { LanguageService } from '../../services/language.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-twitter-widget',
templateUrl: './twitter-widget.component.html',
styleUrls: ['./twitter-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TwitterWidgetComponent implements OnChanges {
@Input() handle: string;
@Input() width = 300;
@Input() height = 400;
loading: boolean = true;
error: boolean = false;
lang: string = 'en';
iframeSrc: SafeResourceUrl;
constructor(
private languageService: LanguageService,
public sanitizer: DomSanitizer,
) {
this.lang = this.languageService.getLanguage();
this.setIframeSrc();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.handle) {
this.setIframeSrc();
}
}
setIframeSrc(): void {
if (this.handle) {
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
`https://syndication.twitter.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
+ '&dnt=true'
+ '&embedId=twitter-widget-0'
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
+ '&frame=false'
+ '&hideBorder=true'
+ '&hideFooter=false'
+ '&hideHeader=true'
+ '&hideScrollBar=false'
+ `&lang=${this.lang}`
+ '&maxHeight=500px'
+ '&origin=https%3A%2F%2Fmempool.space%2F'
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
+ '&showHeader=false'
+ '&showReplies=false'
+ '&siteScreenName=mempool'
+ '&theme=dark'
+ '&transparent=true'
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716'
));
}
}
onReady(): void {
this.loading = false;
this.error = false;
}
onFailed(): void {
this.loading = false;
this.error = true;
}
}

View File

@ -1,6 +1,6 @@
.bowtie-graph-tooltip { .bowtie-graph-tooltip {
position: absolute; position: absolute;
background: rgba(#11131f, 0.95); background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5); box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey); color: var(--tooltip-grey);

View File

@ -84,18 +84,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
refreshOutspends$: ReplaySubject<string> = new ReplaySubject(); refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
gradientColors = { gradientColors = {
'': ['#9339f4', '#105fb0', '#9339f400'], '': ['var(--mainnet-alt)', 'var(--primary)', 'color-mix(in srgb, var(--mainnet-alt) 1%, transparent)'],
// liquid: ['#116761', '#183550'], // liquid: ['#116761', '#183550'],
liquid: ['#09a197', '#0f62af', '#09a19700'], liquid: ['#09a197', '#0f62af', '#09a19700'],
// 'liquidtestnet': ['#494a4a', '#272e46'], // 'liquidtestnet': ['#494a4a', '#272e46'],
'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'], 'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
// testnet: ['#1d486f', '#183550'], // testnet: ['#1d486f', '#183550'],
testnet: ['#4edf77', '#10a0af', '#4edf7700'], testnet: ['#4edf77', '#10a0af', '#4edf7700'],
testnet4: ['#4edf77', '#10a0af', '#4edf7700'],
// signet: ['#6f1d5d', '#471850'], // signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2', '#d24fc800'], signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
}; };
gradient: string[] = ['#105fb0', '#105fb0']; gradient: string[] = ['var(--primary)', 'var(--primary)'];
constructor( constructor(
private router: Router, private router: Router,

View File

@ -301,7 +301,8 @@
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: -13px; margin-top: -13px;
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
@ -435,7 +436,8 @@
.in-progress-message { .in-progress-message {
position: relative; position: relative;
color: #ffffff91; color: var(--fg);
opacity: var(--opacity);
margin-top: 20px; margin-top: 20px;
text-align: center; text-align: center;
padding-bottom: 3px; padding-bottom: 3px;

View File

@ -231,8 +231,10 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
this.stateService.live2Chart$ this.stateService.live2Chart$
.pipe( .pipe(
scan((acc, stats) => { scan((acc, stats) => {
const now = Date.now() / 1000;
const start = now - (2 * 60 * 60);
acc.unshift(stats); acc.unshift(stats);
acc = acc.slice(0, 120); acc = acc.filter(p => p.added >= start);
return acc; return acc;
}, (mempoolStats || [])) }, (mempoolStats || []))
), ),

View File

@ -1,5 +1,6 @@
const bitcoinNetworks = ["", "testnet", "signet"]; const bitcoinNetworks = ["", "testnet", "testnet4", "signet"];
const liquidNetworks = ["liquid", "liquidtestnet"]; const liquidNetworks = ["liquid", "liquidtestnet"];
const lightningNetworks = ["", "testnet", "signet"];
const miningTimeIntervals = "<code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>"; const miningTimeIntervals = "<code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>";
const emptyCodeSample = { const emptyCodeSample = {
@ -6513,7 +6514,7 @@ export const restApiDocsData = [
category: "lightning", category: "lightning",
fragment: "lightning", fragment: "lightning",
title: "Lightning", title: "Lightning",
showConditions: bitcoinNetworks showConditions: lightningNetworks
}, },
{ {
type: "endpoint", type: "endpoint",
@ -6525,7 +6526,7 @@ export const restApiDocsData = [
default: "<p>Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.</p><p>Pass one of the following for <code>:interval</code>: <code>latest</code>, <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>.</p>" default: "<p>Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.</p><p>Pass one of the following for <code>:interval</code>: <code>latest</code>, <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>.</p>"
}, },
urlString: "/v1/lightning/statistics/:interval", urlString: "/v1/lightning/statistics/:interval",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -6621,7 +6622,7 @@ export const restApiDocsData = [
default: "<p>Returns Lightning nodes and channels that match a full-text, case-insensitive search <code>:query</code> across node aliases, node pubkeys, channel IDs, and short channel IDs.</p>" default: "<p>Returns Lightning nodes and channels that match a full-text, case-insensitive search <code>:query</code> across node aliases, node pubkeys, channel IDs, and short channel IDs.</p>"
}, },
urlString: "/v1/lightning/search?searchText=:query", urlString: "/v1/lightning/search?searchText=:query",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -6706,7 +6707,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of Lightning nodes running on clearnet in the requested <code>:country</code>, where <code>:country</code> is an ISO Alpha-2 country code.</p>" default: "<p>Returns a list of Lightning nodes running on clearnet in the requested <code>:country</code>, where <code>:country</code> is an ISO Alpha-2 country code.</p>"
}, },
urlString: "/v1/lightning/nodes/country/:country", urlString: "/v1/lightning/nodes/country/:country",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -6928,7 +6929,7 @@ export const restApiDocsData = [
default: "<p>Returns aggregate capacity and number of clearnet nodes per country. Capacity figures are in satoshis.</p>" default: "<p>Returns aggregate capacity and number of clearnet nodes per country. Capacity figures are in satoshis.</p>"
}, },
urlString: "/v1/lightning/nodes/countries", urlString: "/v1/lightning/nodes/countries",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7072,7 +7073,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of nodes hosted by a specified <code>:isp</code>, where <code>:isp</code> is an ISP's ASN.</p>" default: "<p>Returns a list of nodes hosted by a specified <code>:isp</code>, where <code>:isp</code> is an ISP's ASN.</p>"
}, },
urlString: "/v1/lightning/nodes/isp/:isp", urlString: "/v1/lightning/nodes/isp/:isp",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7191,7 +7192,7 @@ export const restApiDocsData = [
default: "<p>Returns aggregate capacity, number of nodes, and number of channels per ISP. Capacity figures are in satoshis.</p>" default: "<p>Returns aggregate capacity, number of nodes, and number of channels per ISP. Capacity figures are in satoshis.</p>"
}, },
urlString: "/v1/lightning/nodes/isp-ranking", urlString: "/v1/lightning/nodes/isp-ranking",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7303,7 +7304,7 @@ export const restApiDocsData = [
default: "<p>Returns two lists of the top 100 nodes: one ordered by liquidity (aggregate channel capacity) and the other ordered by connectivity (number of open channels).</p>" default: "<p>Returns two lists of the top 100 nodes: one ordered by liquidity (aggregate channel capacity) and the other ordered by connectivity (number of open channels).</p>"
}, },
urlString: "/v1/lightning/nodes/rankings", urlString: "/v1/lightning/nodes/rankings",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7426,7 +7427,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of the top 100 nodes by liquidity (aggregate channel capacity).</p>" default: "<p>Returns a list of the top 100 nodes by liquidity (aggregate channel capacity).</p>"
}, },
urlString: "/v1/lightning/nodes/rankings/liquidity", urlString: "/v1/lightning/nodes/rankings/liquidity",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7623,7 +7624,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of the top 100 nodes by connectivity (number of open channels).</p>" default: "<p>Returns a list of the top 100 nodes by connectivity (number of open channels).</p>"
}, },
urlString: "/v1/lightning/nodes/rankings/connectivity", urlString: "/v1/lightning/nodes/rankings/connectivity",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -7819,7 +7820,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of the top 100 oldest nodes.</p>" default: "<p>Returns a list of the top 100 oldest nodes.</p>"
}, },
urlString: "/v1/lightning/nodes/rankings/age", urlString: "/v1/lightning/nodes/rankings/age",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8006,7 +8007,7 @@ export const restApiDocsData = [
default: "<p>Returns details about a node with the given <code>:pubKey</code>.</p>" default: "<p>Returns details about a node with the given <code>:pubKey</code>.</p>"
}, },
urlString: "/v1/lightning/nodes/:pubKey", urlString: "/v1/lightning/nodes/:pubKey",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8170,7 +8171,7 @@ export const restApiDocsData = [
default: "<p>Returns historical stats for a node with the given <code>:pubKey</code>.</p>" default: "<p>Returns historical stats for a node with the given <code>:pubKey</code>.</p>"
}, },
urlString: "/v1/lightning/nodes/:pubKey/statistics", urlString: "/v1/lightning/nodes/:pubKey/statistics",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8268,7 +8269,7 @@ export const restApiDocsData = [
default: "<p>Returns info about a Lightning channel with the given <code>:channelId</code>.</p>" default: "<p>Returns info about a Lightning channel with the given <code>:channelId</code>.</p>"
}, },
urlString: "/v1/lightning/channels/:channelId", urlString: "/v1/lightning/channels/:channelId",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8433,7 +8434,7 @@ export const restApiDocsData = [
default: "<p>Returns channels that correspond to the given <code>:txid</code> (multiple transaction IDs can be specified).</p>" default: "<p>Returns channels that correspond to the given <code>:txid</code> (multiple transaction IDs can be specified).</p>"
}, },
urlString: "/v1/lightning/channels/txids?txId[]=:txid", urlString: "/v1/lightning/channels/txids?txId[]=:txid",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8634,7 +8635,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of a node's channels given its <code>:pubKey</code>. Ten channels are returned at a time. Use <code>:index</code> for paging. <code>:channelStatus</code> can be <code>open</code>, <code>active</code>, or <code>closed</code>.</p>" default: "<p>Returns a list of a node's channels given its <code>:pubKey</code>. Ten channels are returned at a time. Use <code>:index</code> for paging. <code>:channelStatus</code> can be <code>open</code>, <code>active</code>, or <code>closed</code>.</p>"
}, },
urlString: "/v1/lightning/channels?public_key=:pubKey&status=:channelStatus", urlString: "/v1/lightning/channels?public_key=:pubKey&status=:channelStatus",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8770,7 +8771,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of channels with corresponding node geodata.</p>" default: "<p>Returns a list of channels with corresponding node geodata.</p>"
}, },
urlString: "/v1/lightning/channels-geo", urlString: "/v1/lightning/channels-geo",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {
@ -8878,7 +8879,7 @@ export const restApiDocsData = [
default: "<p>Returns a list of channels with corresponding geodata for a node with the given <code>:pubKey</code>.</p>" default: "<p>Returns a list of channels with corresponding geodata for a node with the given <code>:pubKey</code>.</p>"
}, },
urlString: "/v1/lightning/channels-geo/:pubKey", urlString: "/v1/lightning/channels-geo/:pubKey",
showConditions: bitcoinNetworks, showConditions: lightningNetworks,
showJsExamples: showJsExamplesDefaultFalse, showJsExamples: showJsExamplesDefaultFalse,
codeExample: { codeExample: {
default: { default: {

Some files were not shown because too many files have changed in this diff Show More