mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 14:50:52 +01:00
Merge branch 'master' into nymkappa/prepaid-update-price
This commit is contained in:
commit
0605e80d89
227 changed files with 8470 additions and 1480 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
|
||||
# Latest version available on this commit is 1.71.1
|
||||
# Commit date is Aug 3, 2023
|
||||
uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07
|
||||
uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
|
||||
with:
|
||||
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
||||
|
||||
|
@ -257,7 +257,7 @@ jobs:
|
|||
spec: |
|
||||
cypress/e2e/mainnet/*.spec.ts
|
||||
cypress/e2e/signet/*.spec.ts
|
||||
cypress/e2e/testnet/*.spec.ts
|
||||
cypress/e2e/testnet4/*.spec.ts
|
||||
- module: "liquid"
|
||||
spec: |
|
||||
cypress/e2e/liquid/liquid.spec.ts
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@typescript-eslint/no-this-alias": 1,
|
||||
"@typescript-eslint/no-var-requires": 1,
|
||||
"@typescript-eslint/explicit-function-return-type": 1,
|
||||
"@typescript-eslint/no-unused-vars": 1,
|
||||
"no-console": 1,
|
||||
"no-constant-condition": 1,
|
||||
"no-dupe-else-if": 1,
|
||||
|
@ -32,6 +33,7 @@
|
|||
"prefer-rest-params": 1,
|
||||
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
||||
"semi": 1,
|
||||
"curly": [1, "all"],
|
||||
"eqeqeq": 1
|
||||
}
|
||||
}
|
||||
|
|
14
backend/package-lock.json
generated
14
backend/package-lock.json
generated
|
@ -23,7 +23,7 @@
|
|||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.16.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
|
@ -7690,9 +7690,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
@ -13424,9 +13424,9 @@
|
|||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.16.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
|
@ -22,6 +22,7 @@ export interface AbstractBitcoinApi {
|
|||
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
|
|
|
@ -205,3 +205,16 @@ export namespace IBitcoinApi {
|
|||
"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,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
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 blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
|
@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
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> {
|
||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||
return {
|
||||
|
|
|
@ -55,6 +55,7 @@ class BitcoinRoutes {
|
|||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
|
||||
.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 + 'txs/test', this.$testTransactions)
|
||||
.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/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();
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
|||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
|
@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||
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> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
|
||||
}
|
||||
|
|
|
@ -839,8 +839,11 @@ class Blocks {
|
|||
} else {
|
||||
this.currentBlockHeight++;
|
||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||
await chainTips.updateOrphanedBlocks();
|
||||
// skip updating the orphan block cache if we've fallen behind the chain tip
|
||||
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}`);
|
||||
|
|
|
@ -12,32 +12,68 @@ export interface OrphanedBlock {
|
|||
height: number;
|
||||
hash: string;
|
||||
status: 'valid-fork' | 'valid-headers' | 'headers-only';
|
||||
prevhash: string;
|
||||
}
|
||||
|
||||
class ChainTips {
|
||||
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> {
|
||||
try {
|
||||
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) {
|
||||
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
|
||||
let block = await bitcoinClient.getBlock(chain.hash);
|
||||
while (block && block.confirmations === -1) {
|
||||
this.orphanedBlocks.push({
|
||||
height: block.height,
|
||||
hash: block.hash,
|
||||
status: chain.status
|
||||
});
|
||||
block = await bitcoinClient.getBlock(block.previousblockhash);
|
||||
const orphans: OrphanedBlock[] = [];
|
||||
let hash = chain.hash;
|
||||
do {
|
||||
let orphan = this.blockCache[hash];
|
||||
if (!orphan) {
|
||||
const block = await bitcoinClient.getBlock(hash);
|
||||
if (block && block.confirmations === -1) {
|
||||
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) {
|
||||
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
|
@ -48,13 +84,7 @@ class ChainTips {
|
|||
return [];
|
||||
}
|
||||
|
||||
const orphans: OrphanedBlock[] = [];
|
||||
for (const block of this.orphanedBlocks) {
|
||||
if (block.height === height) {
|
||||
orphans.push(block);
|
||||
}
|
||||
}
|
||||
return orphans;
|
||||
return this.orphansByHeight[height] || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -946,6 +946,33 @@ export class Common {
|
|||
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 {
|
||||
// Do not mutate txhex
|
||||
|
||||
|
|
|
@ -54,9 +54,11 @@ class ChannelsRoutes {
|
|||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||
|
|
|
@ -666,7 +666,9 @@ class NodesApi {
|
|||
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(
|
||||
public_key,
|
||||
first_seen,
|
||||
|
@ -695,13 +697,13 @@ class NodesApi {
|
|||
node.alias,
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
formattedSockets,
|
||||
JSON.stringify(node.features),
|
||||
node.last_update,
|
||||
node.alias,
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
formattedSockets,
|
||||
JSON.stringify(node.features),
|
||||
]);
|
||||
} catch (e) {
|
||||
|
|
|
@ -404,6 +404,10 @@ class Mempool {
|
|||
|
||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
// skip transactions we don't know about
|
||||
if (!this.mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
newAccelerationMap[acceleration.txid] = acceleration;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
|
|
|
@ -24,6 +24,7 @@ class MiningRoutes {
|
|||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees', this.$getBlockFeesTimespan)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||
|
@ -217,6 +218,26 @@ class MiningRoutes {
|
|||
}
|
||||
}
|
||||
|
||||
private async $getBlockFeesTimespan(req: Request, res: Response) {
|
||||
try {
|
||||
if (!parseInt(req.query.from as string, 10) || !parseInt(req.query.to as string, 10)) {
|
||||
throw new Error('Invalid timestamp range');
|
||||
}
|
||||
if (parseInt(req.query.from as string, 10) > parseInt(req.query.to as string, 10)) {
|
||||
throw new Error('from must be less than to');
|
||||
}
|
||||
const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10));
|
||||
const blockCount = await BlocksRepository.$blockCount(null, null);
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.header('X-total-count', blockCount.toString());
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockFees);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getHistoricalBlockRewards(req: Request, res: Response) {
|
||||
try {
|
||||
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
|
||||
|
|
|
@ -45,11 +45,22 @@ class Mining {
|
|||
*/
|
||||
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
|
||||
return await BlocksRepository.$getHistoricalBlockFees(
|
||||
this.getTimeRange(interval, 5),
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timespan block total fees
|
||||
*/
|
||||
public async $getBlockFeesTimespan(from: number, to: number): Promise<number> {
|
||||
return await BlocksRepository.$getHistoricalBlockFees(
|
||||
this.getTimeRangeFromTimespan(from, to),
|
||||
null,
|
||||
{from, to}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get historical block rewards
|
||||
*/
|
||||
|
@ -646,6 +657,24 @@ class Mining {
|
|||
}
|
||||
}
|
||||
|
||||
private getTimeRangeFromTimespan(from: number, to: number, scale = 1): number {
|
||||
const timespan = to - from;
|
||||
switch (true) {
|
||||
case timespan > 3600 * 24 * 365 * 4: return 86400 * scale; // 24h
|
||||
case timespan > 3600 * 24 * 365 * 3: return 43200 * scale; // 12h
|
||||
case timespan > 3600 * 24 * 365 * 2: return 43200 * scale; // 12h
|
||||
case timespan > 3600 * 24 * 365: return 28800 * scale; // 8h
|
||||
case timespan > 3600 * 24 * 30 * 6: return 28800 * scale; // 8h
|
||||
case timespan > 3600 * 24 * 30 * 3: return 10800 * scale; // 3h
|
||||
case timespan > 3600 * 24 * 30: return 7200 * scale; // 2h
|
||||
case timespan > 3600 * 24 * 7: return 1800 * scale; // 30min
|
||||
case timespan > 3600 * 24 * 3: return 300 * scale; // 5min
|
||||
case timespan > 3600 * 24: return 1 * scale;
|
||||
default: return 1 * scale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finds the oldest block in a consecutive chain back from the tip
|
||||
// assumes `blocks` is sorted in ascending height order
|
||||
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
|
|||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
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 (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
|
||||
this.updateSocketData();
|
||||
|
@ -364,6 +376,18 @@ class WebsocketHandler {
|
|||
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) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
@ -524,6 +548,7 @@ class WebsocketHandler {
|
|||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
|
@ -545,6 +570,33 @@ class WebsocketHandler {
|
|||
|
||||
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
|
||||
const socketDataFields = {
|
||||
'mempoolInfo': mempoolInfo,
|
||||
|
@ -604,9 +656,11 @@ class WebsocketHandler {
|
|||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
// pre-compute acceleration delta
|
||||
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
|
||||
for (const server of this.webSocketServers) {
|
||||
|
@ -847,6 +901,18 @@ class WebsocketHandler {
|
|||
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) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
@ -992,6 +1058,31 @@ class WebsocketHandler {
|
|||
|
||||
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 };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
|
@ -1000,10 +1091,6 @@ class WebsocketHandler {
|
|||
return responseCache[key];
|
||||
}
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
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) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
|
|
@ -131,6 +131,7 @@ class Server {
|
|||
})
|
||||
.use(express.urlencoded({ extended: true }))
|
||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||
.use(express.json())
|
||||
;
|
||||
|
||||
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
|
||||
|
|
|
@ -71,6 +71,22 @@ export interface MempoolBlockDelta {
|
|||
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 {
|
||||
scriptsig: string;
|
||||
}
|
||||
|
|
|
@ -663,7 +663,7 @@ class BlocksRepository {
|
|||
/**
|
||||
* Get the historical averaged block fees
|
||||
*/
|
||||
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
|
||||
public async $getHistoricalBlockFees(div: number, interval: string | null, timespan?: {from: number, to: number}): Promise<any> {
|
||||
try {
|
||||
let query = `SELECT
|
||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||
|
@ -677,6 +677,8 @@ class BlocksRepository {
|
|||
|
||||
if (interval !== null) {
|
||||
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||
} else if (timespan) {
|
||||
query += ` WHERE blockTimestamp BETWEEN FROM_UNIXTIME(${timespan.from}) AND FROM_UNIXTIME(${timespan.to})`;
|
||||
}
|
||||
|
||||
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
|
||||
|
|
3
contributors/bitcoinmechanic.txt
Normal file
3
contributors/bitcoinmechanic.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
|
||||
Signed: bitcoinmechanic
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20.12.0-buster-slim AS builder
|
||||
FROM node:20.13.1-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||
|
@ -24,7 +24,7 @@ RUN npm install --omit=dev --omit=optional
|
|||
WORKDIR /build
|
||||
RUN npm run package
|
||||
|
||||
FROM node:20.12.0-buster-slim
|
||||
FROM node:20.13.1-buster-slim
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20.12.0-buster-slim AS builder
|
||||
FROM node:20.13.1-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
|
@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
|||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.25.4-alpine
|
||||
FROM nginx:1.26.0-alpine
|
||||
|
||||
WORKDIR /patch
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"prefer-rest-params": 1,
|
||||
"quotes": [1, "single", { "allowTemplateLiterals": true }],
|
||||
"semi": 1,
|
||||
"curly": [1, "all"],
|
||||
"eqeqeq": 1
|
||||
}
|
||||
}
|
||||
|
|
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
|
@ -63,6 +63,7 @@ src/resources/pools.json
|
|||
src/resources/mining-pools/*
|
||||
src/resources/**/*.mp4
|
||||
src/resources/**/*.vtt
|
||||
src/resources/customize.js
|
||||
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
|
|
|
@ -166,6 +166,7 @@
|
|||
"src/resources",
|
||||
"src/robots.txt",
|
||||
"src/config.js",
|
||||
"src/customize.js",
|
||||
"src/config.template.js"
|
||||
],
|
||||
"styles": [
|
||||
|
@ -180,6 +181,11 @@
|
|||
"bundleName": "wiz",
|
||||
"inject": false
|
||||
},
|
||||
{
|
||||
"input": "src/theme-bukele.scss",
|
||||
"bundleName": "bukele",
|
||||
"inject": false
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
|
|
52
frontend/custom-sv-config.json
Normal file
52
frontend/custom-sv-config.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"theme": "bukele",
|
||||
"enterprise": "onbtc",
|
||||
"branding": {
|
||||
"name": "onbtc",
|
||||
"title": "Bitcoin Office",
|
||||
"site_id": 19,
|
||||
"header_img": "/resources/onbtclogo.svg",
|
||||
"footer_img": "/resources/onbtclogo.svg",
|
||||
"rounded_corner": true
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "balance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "twitter",
|
||||
"mobileOrder": 5,
|
||||
"props": {
|
||||
"handle": "nayibbukele"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "address",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
|
||||
"period": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "addressTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -112,8 +112,8 @@ describe('Mainnet', () => {
|
|||
it('check op_return coinbase tooltip', () => {
|
||||
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('div > a > .badge').first().trigger('onmouseover');
|
||||
cy.get('div > a > .badge').first().trigger('mouseenter');
|
||||
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
|
||||
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
|
||||
cy.get('.tooltip-inner').should('be.visible');
|
||||
});
|
||||
|
||||
|
@ -339,7 +339,7 @@ describe('Mainnet', () => {
|
|||
cy.visit('/');
|
||||
cy.waitForSkeletonGone();
|
||||
|
||||
cy.changeNetwork('testnet');
|
||||
cy.changeNetwork('testnet4');
|
||||
cy.changeNetwork('signet');
|
||||
cy.changeNetwork('mainnet');
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { emitMempoolInfo } from '../../support/websocket';
|
|||
|
||||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
|
||||
describe('Testnet', () => {
|
||||
describe('Testnet4', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/block-height/*').as('block-height');
|
||||
cy.intercept('/api/block/*').as('block');
|
||||
|
@ -13,7 +13,7 @@ describe('Testnet', () => {
|
|||
if (baseModule === 'mempool') {
|
||||
|
||||
it('loads the dashboard', () => {
|
||||
cy.visit('/testnet');
|
||||
cy.visit('/testnet4');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe('Testnet', () => {
|
|||
|
||||
it.skip('loads the dashboard with the skeleton blocks', () => {
|
||||
cy.mockMempoolSocket();
|
||||
cy.visit('/testnet');
|
||||
cy.visit('/testnet4');
|
||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||
|
@ -45,7 +45,7 @@ describe('Testnet', () => {
|
|||
});
|
||||
|
||||
it('loads the pools screen', () => {
|
||||
cy.visit('/testnet');
|
||||
cy.visit('/testnet4');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-pools').click().then(() => {
|
||||
cy.wait(1000);
|
||||
|
@ -53,7 +53,7 @@ describe('Testnet', () => {
|
|||
});
|
||||
|
||||
it('loads the graphs screen', () => {
|
||||
cy.visit('/testnet');
|
||||
cy.visit('/testnet4');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-graphs').click().then(() => {
|
||||
cy.wait(1000);
|
||||
|
@ -63,7 +63,7 @@ describe('Testnet', () => {
|
|||
describe('tv mode', () => {
|
||||
it('loads the tv screen - desktop', () => {
|
||||
cy.viewport('macbook-16');
|
||||
cy.visit('/testnet/graphs');
|
||||
cy.visit('/testnet4/graphs');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-tv').click().then(() => {
|
||||
cy.wait(1000);
|
||||
|
@ -73,7 +73,7 @@ describe('Testnet', () => {
|
|||
});
|
||||
|
||||
it('loads the tv screen - mobile', () => {
|
||||
cy.visit('/testnet/graphs');
|
||||
cy.visit('/testnet4/graphs');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-tv').click().then(() => {
|
||||
cy.viewport('iphone-6');
|
||||
|
@ -85,7 +85,7 @@ describe('Testnet', () => {
|
|||
|
||||
|
||||
it('loads the api screen', () => {
|
||||
cy.visit('/testnet');
|
||||
cy.visit('/testnet4');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#btn-docs').click().then(() => {
|
||||
cy.wait(1000);
|
||||
|
@ -94,13 +94,13 @@ describe('Testnet', () => {
|
|||
|
||||
describe('blocks', () => {
|
||||
it('shows empty blocks properly', () => {
|
||||
cy.visit('/testnet/block/0');
|
||||
cy.visit('/testnet4/block/0');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||
});
|
||||
|
||||
it('expands and collapses the block details', () => {
|
||||
cy.visit('/testnet/block/0');
|
||||
cy.visit('/testnet4/block/0');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||
cy.get('#details').should('be.visible');
|
||||
|
@ -112,15 +112,15 @@ describe('Testnet', () => {
|
|||
});
|
||||
|
||||
it('shows blocks with no pagination', () => {
|
||||
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
|
||||
cy.visit('/testnet4/block/000000000066e8b6cc78a93f8989587f5819624bae2eb1c05f535cadded19f99');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('h2').invoke('text').should('equal', '11 transactions');
|
||||
cy.get('h2').invoke('text').should('equal', '18 transactions');
|
||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||
});
|
||||
|
||||
it('supports pagination on the block screen', () => {
|
||||
// 48 txs
|
||||
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
|
||||
cy.visit('/testnet4/block/000000000000006982d53f8273bdff21dafc380c292eabc669b5ab6d732311c3');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||
cy.get('.active + li').first().click().then(() => {
|
|
@ -72,7 +72,7 @@ Cypress.Commands.add('mockMempoolSocket', () => {
|
|||
mockWebSocket();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => {
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
|
||||
cy.get('.dropdown-toggle').click().then(() => {
|
||||
cy.get(`a.${network}`).click().then(() => {
|
||||
cy.waitForPageIdle();
|
||||
|
|
2
frontend/cypress/support/index.d.ts
vendored
2
frontend/cypress/support/index.d.ts
vendored
|
@ -5,6 +5,6 @@ declare namespace Cypress {
|
|||
waitForSkeletonGone(): Chainable<any>
|
||||
waitForPageIdle(): Chainable<any>
|
||||
mockMempoolSocket(): Chainable<any>
|
||||
changeNetwork(network: "testnet"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
||||
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
||||
}
|
||||
}
|
|
@ -4,11 +4,14 @@ const { spawnSync } = require('child_process');
|
|||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||
const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js';
|
||||
|
||||
let settings = [];
|
||||
let configContent = {};
|
||||
let gitCommitHash = '';
|
||||
let packetJsonVersion = '';
|
||||
let customConfig;
|
||||
let customConfigContent;
|
||||
|
||||
try {
|
||||
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||
|
@ -22,7 +25,18 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
|
||||
if (configContent && configContent.CUSTOMIZATION) {
|
||||
try {
|
||||
customConfig = readConfig(configContent.CUSTOMIZATION);
|
||||
customConfigContent = JSON.parse(customConfig);
|
||||
} catch (e) {
|
||||
console.log(`failed to load customization config from ${configContent.CUSTOMIZATION}`);
|
||||
}
|
||||
}
|
||||
|
||||
const baseModuleName = configContent.BASE_MODULE || 'mempool';
|
||||
const customBuildName = (customConfigContent && customConfigContent.enterprise) ? ('.' + customConfigContent.enterprise) : '';
|
||||
const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html';
|
||||
|
||||
try {
|
||||
fs.copyFileSync(indexFilePath, 'src/index.html');
|
||||
|
@ -109,6 +123,17 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
|||
|
||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||
|
||||
let customConfigJs = '';
|
||||
if (customConfig) {
|
||||
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
|
||||
customConfigJs = `(function (window) {
|
||||
window.__env = window.__env || {};
|
||||
window.__env.customize = ${customConfig};
|
||||
}((typeof global !== 'undefined') ? global : this));
|
||||
`;
|
||||
}
|
||||
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
|
||||
|
||||
if (currentConfig && currentConfig === newConfig) {
|
||||
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"TESTNET_ENABLED": false,
|
||||
"TESTNET4_ENABLED": false,
|
||||
"SIGNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"LIQUID_TESTNET_ENABLED": false,
|
||||
|
|
398
frontend/package-lock.json
generated
398
frontend/package-lock.json
generated
|
@ -32,10 +32,10 @@
|
|||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cypress": "^13.8.0",
|
||||
"cypress": "^13.9.0",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.5.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.21.1",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~17.1.0",
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
|
@ -63,7 +63,7 @@
|
|||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.8.0",
|
||||
"cypress": "^13.9.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
@ -3197,9 +3197,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
@ -3212,9 +3212,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -3227,9 +3227,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -3242,9 +3242,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3257,9 +3257,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -3272,9 +3272,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3287,9 +3287,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -3302,9 +3302,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3317,9 +3317,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -3332,9 +3332,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -3347,9 +3347,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -3362,9 +3362,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
|
||||
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
@ -3377,9 +3377,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
|
||||
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
@ -3392,9 +3392,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
@ -3407,9 +3407,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
|
||||
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
@ -3422,9 +3422,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
|
||||
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
@ -3437,9 +3437,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3452,9 +3452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3467,9 +3467,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3482,9 +3482,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -3497,9 +3497,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -3512,9 +3512,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -3527,9 +3527,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -8029,9 +8029,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz",
|
||||
"integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==",
|
||||
"version": "13.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
|
||||
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
@ -9197,9 +9197,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
|
||||
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
|
@ -9208,29 +9208,29 @@
|
|||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.1",
|
||||
"@esbuild/android-arm": "0.21.1",
|
||||
"@esbuild/android-arm64": "0.21.1",
|
||||
"@esbuild/android-x64": "0.21.1",
|
||||
"@esbuild/darwin-arm64": "0.21.1",
|
||||
"@esbuild/darwin-x64": "0.21.1",
|
||||
"@esbuild/freebsd-arm64": "0.21.1",
|
||||
"@esbuild/freebsd-x64": "0.21.1",
|
||||
"@esbuild/linux-arm": "0.21.1",
|
||||
"@esbuild/linux-arm64": "0.21.1",
|
||||
"@esbuild/linux-ia32": "0.21.1",
|
||||
"@esbuild/linux-loong64": "0.21.1",
|
||||
"@esbuild/linux-mips64el": "0.21.1",
|
||||
"@esbuild/linux-ppc64": "0.21.1",
|
||||
"@esbuild/linux-riscv64": "0.21.1",
|
||||
"@esbuild/linux-s390x": "0.21.1",
|
||||
"@esbuild/linux-x64": "0.21.1",
|
||||
"@esbuild/netbsd-x64": "0.21.1",
|
||||
"@esbuild/openbsd-x64": "0.21.1",
|
||||
"@esbuild/sunos-x64": "0.21.1",
|
||||
"@esbuild/win32-arm64": "0.21.1",
|
||||
"@esbuild/win32-ia32": "0.21.1",
|
||||
"@esbuild/win32-x64": "0.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-wasm": {
|
||||
|
@ -20563,141 +20563,141 @@
|
|||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
|
||||
},
|
||||
"@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
|
||||
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
|
||||
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
|
||||
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
|
||||
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
|
||||
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
|
||||
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
|
||||
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
|
||||
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
|
||||
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
|
||||
"optional": true
|
||||
},
|
||||
"@eslint-community/eslint-utils": {
|
||||
|
@ -24112,9 +24112,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz",
|
||||
"integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==",
|
||||
"version": "13.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
|
||||
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
|
@ -25032,33 +25032,33 @@
|
|||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
|
||||
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
|
||||
"requires": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.1",
|
||||
"@esbuild/android-arm": "0.21.1",
|
||||
"@esbuild/android-arm64": "0.21.1",
|
||||
"@esbuild/android-x64": "0.21.1",
|
||||
"@esbuild/darwin-arm64": "0.21.1",
|
||||
"@esbuild/darwin-x64": "0.21.1",
|
||||
"@esbuild/freebsd-arm64": "0.21.1",
|
||||
"@esbuild/freebsd-x64": "0.21.1",
|
||||
"@esbuild/linux-arm": "0.21.1",
|
||||
"@esbuild/linux-arm64": "0.21.1",
|
||||
"@esbuild/linux-ia32": "0.21.1",
|
||||
"@esbuild/linux-loong64": "0.21.1",
|
||||
"@esbuild/linux-mips64el": "0.21.1",
|
||||
"@esbuild/linux-ppc64": "0.21.1",
|
||||
"@esbuild/linux-riscv64": "0.21.1",
|
||||
"@esbuild/linux-s390x": "0.21.1",
|
||||
"@esbuild/linux-x64": "0.21.1",
|
||||
"@esbuild/netbsd-x64": "0.21.1",
|
||||
"@esbuild/openbsd-x64": "0.21.1",
|
||||
"@esbuild/sunos-x64": "0.21.1",
|
||||
"@esbuild/win32-arm64": "0.21.1",
|
||||
"@esbuild/win32-ia32": "0.21.1",
|
||||
"@esbuild/win32-x64": "0.21.1"
|
||||
}
|
||||
},
|
||||
"esbuild-wasm": {
|
||||
|
|
|
@ -50,16 +50,16 @@
|
|||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "npm run generate-config && node server.run.js",
|
||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||
"prerender": "npm run ng -- run mempool:prerender",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"cypress:run:record": "cypress run --record",
|
||||
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
|
||||
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
|
||||
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
|
||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
|
||||
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
|
||||
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
|
||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.1",
|
||||
|
@ -92,7 +92,7 @@
|
|||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.21.1",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.8.0",
|
||||
"cypress": "^13.9.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
|
|
@ -24,7 +24,7 @@ PROXY_CONFIG = [
|
|||
'/api/**', '!/api/v1/ws',
|
||||
'!/liquid', '!/liquid/**', '!/liquid/',
|
||||
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
|
||||
'/testnet/api/**', '/signet/api/**'
|
||||
'/testnet/api/**', '/signet/api/**', '/testnet4/api/**'
|
||||
],
|
||||
target: "https://mempool.space",
|
||||
ws: true,
|
||||
|
|
|
@ -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',
|
||||
children: [
|
||||
|
@ -130,6 +168,10 @@ let routes: Routes = [
|
|||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
|
|
|
@ -189,22 +189,22 @@ export const specialBlocks = {
|
|||
'0': {
|
||||
labelEvent: 'Genesis',
|
||||
labelEventCompleted: 'The Genesis of Bitcoin',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'210000': {
|
||||
labelEvent: 'Bitcoin\'s 1st Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'420000': {
|
||||
labelEvent: 'Bitcoin\'s 2nd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'630000': {
|
||||
labelEvent: 'Bitcoin\'s 3rd Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'709632': {
|
||||
labelEvent: 'Taproot 🌱 activation',
|
||||
|
@ -214,62 +214,62 @@ export const specialBlocks = {
|
|||
'840000': {
|
||||
labelEvent: 'Bitcoin\'s 4th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1050000': {
|
||||
labelEvent: 'Bitcoin\'s 5th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1260000': {
|
||||
labelEvent: 'Bitcoin\'s 6th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1470000': {
|
||||
labelEvent: 'Bitcoin\'s 7th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1680000': {
|
||||
labelEvent: 'Bitcoin\'s 8th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'1890000': {
|
||||
labelEvent: 'Bitcoin\'s 9th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2100000': {
|
||||
labelEvent: 'Bitcoin\'s 10th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2310000': {
|
||||
labelEvent: 'Bitcoin\'s 11th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2520000': {
|
||||
labelEvent: 'Bitcoin\'s 12th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2730000': {
|
||||
labelEvent: 'Bitcoin\'s 13th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'2940000': {
|
||||
labelEvent: 'Bitcoin\'s 14th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
},
|
||||
'3150000': {
|
||||
labelEvent: 'Bitcoin\'s 15th Halving',
|
||||
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
|
||||
networks: ['mainnet', 'testnet'],
|
||||
networks: ['mainnet', 'testnet', 'testnet4'],
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -266,6 +266,11 @@ const featureActivation = {
|
|||
segwit: 872730,
|
||||
taproot: 2032291,
|
||||
},
|
||||
testnet4: {
|
||||
rbf: 0,
|
||||
segwit: 0,
|
||||
taproot: 0,
|
||||
},
|
||||
signet: {
|
||||
rbf: 0,
|
||||
segwit: 0,
|
||||
|
|
|
@ -343,8 +343,8 @@
|
|||
<a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance">
|
||||
<img class="copa" src="/resources/profile/copa.png" />
|
||||
</a>
|
||||
<a href="https://bisq.network/" title="Bisq Network">
|
||||
<img class="bisq" src="/resources/profile/bisq.svg" />
|
||||
<a href="https://bitcoin.gob.sv" title="Oficina Nacional del Bitcoin">
|
||||
<img class="sv" src="/resources/profile/onbtc-full.svg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -129,8 +129,9 @@
|
|||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
.bisq {
|
||||
top: 3px;
|
||||
.sv {
|
||||
height: 85px;
|
||||
width: auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<span class="font-weight-bold">Accelerate</span>
|
||||
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected within ~30 minutes<br>
|
||||
@if (!calculating) {
|
||||
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats|sats">sats</span></span>)
|
||||
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>)
|
||||
} @else {
|
||||
<span class="estimating">Calculating cost...</span>
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
|
@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service';
|
|||
styleUrls: ['./accelerations-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationsListComponent implements OnInit {
|
||||
export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() pending: boolean = false;
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
|
@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit {
|
|||
|
||||
this.accelerationList$ = this.pageSubject.pipe(
|
||||
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(
|
||||
switchMap(response => {
|
||||
let accelerations = response;
|
||||
|
@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit {
|
|||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackAccelerations();
|
||||
}
|
||||
}
|
|
@ -60,7 +60,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -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 { OpenGraphService } from '../../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
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 { hexToColor } from '../../block-overview-graph/utils';
|
||||
import TxView from '../../block-overview-graph/tx-view';
|
||||
|
@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended {
|
|||
styleUrls: ['./accelerator-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AcceleratorDashboardComponent implements OnInit {
|
||||
export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
blocks$: Observable<AccelerationBlock[]>;
|
||||
accelerations$: Observable<Acceleration[]>;
|
||||
pendingAccelerations$: Observable<Acceleration[]>;
|
||||
|
@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
firstLoad = true;
|
||||
timespan: '3d' | '1w' | '1m' = '1w';
|
||||
|
||||
accelerationDeltaSubscription: Subscription;
|
||||
|
||||
graphHeight: number = 300;
|
||||
theme: ThemeService;
|
||||
|
||||
|
@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
this.websocketService.startTrackAccelerations();
|
||||
|
||||
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).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;
|
||||
}),
|
||||
this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe(
|
||||
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(
|
||||
distinctUntilChanged(),
|
||||
|
@ -145,7 +148,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
} else {
|
||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
||||
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;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.accelerationDeltaSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackAccelerations();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
|
|
|
@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
|
|||
import { Observable, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
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({
|
||||
selector: 'app-pending-stats',
|
||||
|
@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit {
|
|||
public accelerationStats$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe(
|
||||
this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe(
|
||||
switchMap(accelerations => {
|
||||
let totalAccelerations = 0;
|
||||
let totalFeeDelta = 0;
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
<app-indexing-progress></app-indexing-progress>
|
||||
|
||||
<div class="full-container">
|
||||
<div 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>
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div [class.full-container]="!widget">
|
||||
<ng-container *ngIf="!error">
|
||||
<div class="chart" *browserOnly 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)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
|
@ -20,4 +14,8 @@
|
|||
<p class="error">{{ error }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
@ -45,28 +46,12 @@
|
|||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
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 { of } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
const periodSeconds = {
|
||||
'1d': (60 * 60 * 24),
|
||||
'3d': (60 * 60 * 24 * 3),
|
||||
'1w': (60 * 60 * 24 * 7),
|
||||
'1m': (60 * 60 * 24 * 30),
|
||||
'6m': (60 * 60 * 24 * 180),
|
||||
'1y': (60 * 60 * 24 * 365),
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-graph',
|
||||
|
@ -22,16 +32,23 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
|||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddressGraphComponent implements OnChanges {
|
||||
export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
@Input() address: string;
|
||||
@Input() isPubkey: boolean = false;
|
||||
@Input() stats: ChainStats;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() period: '1d' | '3d' | '1w' | '1m' | '6m' | '1y' | 'all' = 'all';
|
||||
@Input() height: number = 200;
|
||||
@Input() right: number | string = 10;
|
||||
@Input() left: number | string = 70;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
data: any[] = [];
|
||||
hoverData: any[] = [];
|
||||
|
||||
subscription: Subscription;
|
||||
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
|
@ -43,6 +60,7 @@ export class AddressGraphComponent implements OnChanges {
|
|||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private router: Router,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
|
@ -52,32 +70,59 @@ export class AddressGraphComponent implements OnChanges {
|
|||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
(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(addressSummary => {
|
||||
if (addressSummary) {
|
||||
this.error = null;
|
||||
this.prepareChartOptions(addressSummary);
|
||||
if (!this.address || !this.stats) {
|
||||
return;
|
||||
}
|
||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.subscription = combineLatest([
|
||||
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 {
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); // + (summary[0]?.value || 0);
|
||||
if (!summary || !this.stats) {
|
||||
return;
|
||||
}
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||
this.data = summary.map(d => {
|
||||
const balance = total;
|
||||
total -= d.value;
|
||||
return [d.time * 1000, balance, d];
|
||||
}).reverse();
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1])), 0);
|
||||
if (this.period !== 'all') {
|
||||
const now = Date.now();
|
||||
const start = now - (periodSeconds[this.period] * 1000);
|
||||
this.data = this.data.filter(d => d[0] >= start);
|
||||
this.data.push(
|
||||
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
this.chartOptions = {
|
||||
color: [
|
||||
|
@ -108,6 +153,9 @@ export class AddressGraphComponent implements OnChanges {
|
|||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (data): string {
|
||||
if (!data?.length || !data[0]?.data?.[2]?.txid) {
|
||||
return '';
|
||||
}
|
||||
const header = data.length === 1
|
||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||
: `${data.length} transactions`;
|
||||
|
@ -141,13 +189,17 @@ export class AddressGraphComponent implements OnChanges {
|
|||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val): string => {
|
||||
if (maxValue > 1_000_000_000) {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
if (valSpan > 100_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
|
||||
} else if (maxValue > 100_000_000) {
|
||||
}
|
||||
else if (valSpan > 1_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
|
||||
} else if (valSpan > 100_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||
} else if (maxValue > 10_000_000) {
|
||||
} else if (valSpan > 10_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||
} else if (maxValue > 1_000_000) {
|
||||
} else if (valSpan > 1_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||
} else {
|
||||
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
|
||||
|
@ -157,6 +209,7 @@ export class AddressGraphComponent implements OnChanges {
|
|||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
min: this.period === 'all' ? 0 : 'dataMin'
|
||||
},
|
||||
],
|
||||
series: [
|
||||
|
@ -194,6 +247,12 @@ export class AddressGraphComponent implements OnChanges {
|
|||
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<table class="table latest-transactions">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat">{{ currency }}</th>
|
||||
<th class="table-cell-date" i18n="shared.date">Date</th>
|
||||
</thead>
|
||||
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
|
||||
<tr *ngFor="let transaction of transactions; let i = index;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true"></app-time></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<div class=""> </div>
|
||||
</table>
|
||||
|
||||
<ng-template #recentTransactionsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fiat"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
|
@ -0,0 +1,50 @@
|
|||
.latest-transactions {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-satoshis {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fiat {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-date {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.skeleton-loader-transactions {
|
||||
max-width: 250px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: -3px;
|
||||
height: 18px;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-transactions-widget',
|
||||
templateUrl: './address-transactions-widget.component.html',
|
||||
styleUrls: ['./address-transactions-widget.component.scss'],
|
||||
})
|
||||
export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() address: string;
|
||||
@Input() addressInfo: Address;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() isPubkey: boolean = false;
|
||||
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
transactions$: Observable<any[]>;
|
||||
|
||||
isLoading: boolean = true;
|
||||
error: any;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private priceService: PriceService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
startAddressSubscription(): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
return;
|
||||
}
|
||||
this.transactions$ = (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 history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
||||
return of(null);
|
||||
})
|
||||
)).pipe(
|
||||
map(summary => {
|
||||
return summary?.slice(0, 6);
|
||||
}),
|
||||
switchMap(txs => {
|
||||
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe(
|
||||
map(price => {
|
||||
return {
|
||||
...tx,
|
||||
price,
|
||||
};
|
||||
})
|
||||
))));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: var(--fg);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
|
|
@ -53,10 +53,20 @@
|
|||
|
||||
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left" i18n="address.balance-history">Balance History</h2>
|
||||
</div>
|
||||
<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="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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.qr-wrapper {
|
||||
background-color: var(--fg);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
@ -109,3 +109,19 @@ h1 {
|
|||
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);
|
||||
}
|
|
@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
now = Date.now() / 1000;
|
||||
balancePeriod: 'all' | '1m' = 'all';
|
||||
|
||||
private tempTransactions: Transaction[];
|
||||
private timeTxIndexes: number[];
|
||||
|
@ -173,7 +175,14 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.transactions = this.tempTransactions;
|
||||
if (this.transactions.length === this.txCount) {
|
||||
this.fullyLoaded = true;
|
||||
}
|
||||
this.isLoadingTransactions = false;
|
||||
|
||||
if (!this.showBalancePeriod()) {
|
||||
this.setBalancePeriod('all');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
|
@ -296,6 +305,18 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||
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() {
|
||||
this.mainSubscription.unsubscribe();
|
||||
this.mempoolTxSubscription.unsubscribe();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
|
||||
(
|
||||
|
@ -20,10 +20,29 @@
|
|||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||
<span i18n="shared.confidential">Confidential</span>
|
||||
</ng-template>
|
||||
<ng-template #default>‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
|
||||
<ng-template #default>
|
||||
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol">
|
||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>BTC
|
||||
</span>
|
||||
} @else {
|
||||
@if (digitsInfo === '1.8-8') {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
||||
} @else {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
|
||||
}
|
||||
<span class="symbol">
|
||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
||||
</span>
|
||||
}
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #prefix>
|
||||
<ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</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>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Price } from '../../services/price.service';
|
|||
export class AmountComponent implements OnInit, OnDestroy {
|
||||
conversions$: Observable<any>;
|
||||
currency: string;
|
||||
viewFiat$: Observable<boolean>;
|
||||
viewAmountMode$: Observable<'btc' | 'sats' | 'fiat'>;
|
||||
network = '';
|
||||
|
||||
stateSubscription: Subscription;
|
||||
|
@ -37,7 +37,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||
this.viewAmountMode$ = this.stateService.viewAmountMode$.asObservable();
|
||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||
this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import { Router, NavigationEnd } from '@angular/router';
|
|||
import { StateService } from '../../services/state.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -12,12 +14,12 @@ import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
|
|||
providers: [NgbTooltipConfig]
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
link: HTMLElement = document.getElementById('canonical');
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private stateService: StateService,
|
||||
private openGraphService: OpenGraphService,
|
||||
private seoService: SeoService,
|
||||
private themeService: ThemeService,
|
||||
private location: Location,
|
||||
tooltipConfig: NgbTooltipConfig,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
|
@ -52,11 +54,7 @@ export class AppComponent implements OnInit {
|
|||
ngOnInit() {
|
||||
this.router.events.subscribe((val) => {
|
||||
if (val instanceof NavigationEnd) {
|
||||
let domain = 'mempool.space';
|
||||
if (this.stateService.env.BASE_MODULE === 'liquid') {
|
||||
domain = 'liquid.network';
|
||||
}
|
||||
this.link.setAttribute('href', 'https://' + domain + this.location.path());
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.qr-wrapper {
|
||||
background-color: var(--fg);
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<div class="balance-container" *ngIf="!isLoading; else loading">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||
<div class="card-text">
|
||||
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
|
||||
<div class="card-text">
|
||||
{{ delta7d > 0 ? '+' : ''}}{{ ((delta7d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="delta7d"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
|
||||
<div class="card-text">
|
||||
{{ delta30d > 0 ? '+' : ''}}{{ ((delta30d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="delta30d"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="balance-skeleton">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,160 @@
|
|||
.balance-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
.shared-block {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
.item {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
&:last-child {
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.balance-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:last-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 0;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg);
|
||||
height: 126px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--title-fg);
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: var(--secondary);
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { Observable, catchError, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-balance-widget',
|
||||
templateUrl: './balance-widget.component.html',
|
||||
styleUrls: ['./balance-widget.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BalanceWidgetComponent implements OnInit, OnChanges {
|
||||
@Input() address: string;
|
||||
@Input() addressInfo: Address;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() isPubkey: boolean = false;
|
||||
|
||||
isLoading: boolean = true;
|
||||
error: any;
|
||||
|
||||
delta7d: number = 0;
|
||||
delta30d: number = 0;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
return;
|
||||
}
|
||||
(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(addressSummary => {
|
||||
if (addressSummary) {
|
||||
this.error = null;
|
||||
this.calculateStats(addressSummary);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
calculateStats(summary: AddressTxSummary[]): void {
|
||||
let weekTotal = 0;
|
||||
let monthTotal = 0;
|
||||
|
||||
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++) {
|
||||
monthTotal += summary[i].value;
|
||||
if (summary[i].time >= weekAgo) {
|
||||
weekTotal += summary[i].value;
|
||||
}
|
||||
}
|
||||
this.delta7d = weekTotal;
|
||||
this.delta30d = monthTotal;
|
||||
}
|
||||
}
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<app-indexing-progress></app-indexing-progress>
|
||||
|
||||
<div class="full-container">
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="mining.block-fees-subsidy-subsidy">Block Fees Vs Subsidy</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||
</label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" [style]="{opacity: isLoading ? 0.5 : 1}"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,66 @@
|
|||
.card-header {
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-fees-subsidy-graph',
|
||||
templateUrl: './block-fees-subsidy-graph.component.html',
|
||||
styleUrls: ['./block-fees-subsidy-graph.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: UntypedFormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
statsObservable$: Observable<any>;
|
||||
data: any;
|
||||
subsidies: { [key: number]: number } = {};
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
showFiat = false;
|
||||
updateZoom = false;
|
||||
zoomSpan = 100;
|
||||
zoomTimeSpan = '';
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
public stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
|
||||
this.subsidies = this.initSubsidies();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@mining.block-fees-subsidy:Block Fees Vs Subsidy`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees-subsidy:See the mining fees earned per Bitcoin block compared to the Bitcoin block subsidy, visualized in BTC and USD over time.`);
|
||||
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.route
|
||||
.fragment
|
||||
.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
|
||||
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||
switchMap((timespan) => {
|
||||
this.isLoading = true;
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
this.timespan = timespan;
|
||||
this.zoomTimeSpan = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalBlockFees$(timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.data = {
|
||||
timestamp: response.body.map(val => val.timestamp * 1000),
|
||||
blockHeight: response.body.map(val => val.avgHeight),
|
||||
blockFees: response.body.map(val => val.avgFees / 100_000_000),
|
||||
blockFeesFiat: response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD']),
|
||||
blockSubsidy: response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000),
|
||||
blockSubsidyFiat: response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD']),
|
||||
};
|
||||
|
||||
this.prepareChartOptions();
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions() {
|
||||
let title: object;
|
||||
if (this.data.blockFees.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
color: [
|
||||
'#ff9f00',
|
||||
'#0aab2f',
|
||||
],
|
||||
animation: false,
|
||||
grid: {
|
||||
top: 80,
|
||||
bottom: 80,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)',
|
||||
textStyle: {
|
||||
color: 'var(--tooltip-grey)',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: 'var(--active-bg)',
|
||||
formatter: function (data) {
|
||||
if (data.length <= 0) {
|
||||
return '';
|
||||
}
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.zoomTimeSpan, parseInt(this.data.timestamp[data[0].dataIndex], 10))}</b><br>`;
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const tick = data[i];
|
||||
if (!this.showFiat) tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data, this.locale, '1.0-3')} BTC<br>`;
|
||||
else tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data, null, 'USD') }<br>`;
|
||||
}
|
||||
if (!this.showFiat) tooltip += `<div style="margin-left: 2px">${formatNumber(data.reduce((acc, val) => acc + val.data, 0), this.locale, '1.0-3')} BTC</div>`;
|
||||
else tooltip += `<div style="margin-left: 2px">${this.fiatCurrencyPipe.transform(data.reduce((acc, val) => acc + val.data, 0), null, 'USD')}</div>`;
|
||||
if (['24h', '3d'].includes(this.zoomTimeSpan)) {
|
||||
tooltip += `<small>` + $localize`At block <b style="color: white; margin-left: 2px">${data[0].axisValue}` + `</small>`;
|
||||
} else {
|
||||
tooltip += `<small>` + $localize`Around block <b style="color: white; margin-left: 2px">${data[0].axisValue}` + `</small>`;
|
||||
}
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: this.data.blockFees.length === 0 ? undefined : [
|
||||
{
|
||||
type: 'category',
|
||||
data: this.data.blockHeight,
|
||||
show: false,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
data: this.data.timestamp,
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
axisLabel: {
|
||||
color: 'var(--grey)',
|
||||
formatter: (val) => {
|
||||
return formatterXAxis(this.locale, this.timespan, parseInt(val, 10));
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
}
|
||||
],
|
||||
legend: this.data.blockFees.length === 0 ? undefined : {
|
||||
data: [
|
||||
{
|
||||
name: 'Subsidy',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Fees',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Subsidy (USD)',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Fees (USD)',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
selected: {
|
||||
'Subsidy (USD)': this.showFiat,
|
||||
'Fees (USD)': this.showFiat,
|
||||
'Subsidy': !this.showFiat,
|
||||
'Fees': !this.showFiat,
|
||||
},
|
||||
},
|
||||
yAxis: this.data.blockFees.length === 0 ? undefined : [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'var(--grey)',
|
||||
formatter: (val) => {
|
||||
return `${val} BTC`;
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: 'var(--transparent-fg)',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'var(--grey)',
|
||||
formatter: function(val) {
|
||||
return this.fiatShortenerPipe.transform(val, null, 'USD');
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: this.data.blockFees.length === 0 ? undefined : [
|
||||
{
|
||||
name: 'Subsidy',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: this.data.blockSubsidy,
|
||||
},
|
||||
{
|
||||
name: 'Fees',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: this.data.blockFees,
|
||||
},
|
||||
{
|
||||
name: 'Subsidy (USD)',
|
||||
yAxisIndex: 1,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: this.data.blockSubsidyFiat,
|
||||
},
|
||||
{
|
||||
name: 'Fees (USD)',
|
||||
yAxisIndex: 1,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: this.data.blockFeesFiat,
|
||||
},
|
||||
],
|
||||
dataZoom: this.data.blockFees.length === 0 ? undefined : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
maxSpan: 100,
|
||||
minSpan: 1,
|
||||
moveOnMouseMove: false,
|
||||
}, {
|
||||
showDetail: false,
|
||||
show: true,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('legendselectchanged', (params) => {
|
||||
const isFiat = params.name.includes('USD');
|
||||
if (isFiat === this.showFiat) return;
|
||||
|
||||
const isActivation = params.selected[params.name];
|
||||
if (isFiat === isActivation) {
|
||||
this.showFiat = true;
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees (USD)' });
|
||||
} else {
|
||||
this.showFiat = false;
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees (USD)' });
|
||||
}
|
||||
});
|
||||
|
||||
this.chartInstance.on('datazoom', (params) => {
|
||||
if (params.silent || this.isLoading || ['24h', '3d'].includes(this.timespan)) {
|
||||
return;
|
||||
}
|
||||
this.updateZoom = true;
|
||||
});
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
this.zone.run(() => {
|
||||
if (['24h', '3d'].includes(this.zoomTimeSpan)) {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.name}`);
|
||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||
window.open(url);
|
||||
} else {
|
||||
this.router.navigate([url]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('document:pointerup', ['$event'])
|
||||
onPointerUp(event: PointerEvent) {
|
||||
if (this.updateZoom) {
|
||||
this.onZoom();
|
||||
this.updateZoom = false;
|
||||
}
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
initSubsidies(): { [key: number]: number } {
|
||||
let blockReward = 50 * 100_000_000;
|
||||
const subsidies = {};
|
||||
for (let i = 0; i <= 33; i++) {
|
||||
subsidies[i] = blockReward;
|
||||
blockReward = Math.floor(blockReward / 2);
|
||||
}
|
||||
return subsidies;
|
||||
}
|
||||
|
||||
onZoom() {
|
||||
const option = this.chartInstance.getOption();
|
||||
const timestamps = option.xAxis[1].data;
|
||||
const startTimestamp = timestamps[option.dataZoom[0].startValue];
|
||||
const endTimestamp = timestamps[option.dataZoom[0].endValue];
|
||||
|
||||
this.isLoading = true;
|
||||
this.cd.detectChanges();
|
||||
|
||||
const subscription = this.apiService.getBlockFeesFromTimespan$(Math.floor(startTimestamp / 1000), Math.floor(endTimestamp / 1000))
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
const startIndex = option.dataZoom[0].startValue;
|
||||
const endIndex = option.dataZoom[0].endValue;
|
||||
|
||||
// Update series with more granular data
|
||||
const lengthBefore = this.data.timestamp.length;
|
||||
this.data.timestamp.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.timestamp * 1000));
|
||||
this.data.blockHeight.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgHeight));
|
||||
this.data.blockFees.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgFees / 100_000_000));
|
||||
this.data.blockFeesFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD']));
|
||||
this.data.blockSubsidy.splice(startIndex, endIndex - startIndex, ...response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000));
|
||||
this.data.blockSubsidyFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD']));
|
||||
option.series[0].data = this.data.blockSubsidy;
|
||||
option.series[1].data = this.data.blockFees;
|
||||
option.series[2].data = this.data.blockSubsidyFiat;
|
||||
option.series[3].data = this.data.blockFeesFiat;
|
||||
option.xAxis[0].data = this.data.blockHeight;
|
||||
option.xAxis[1].data = this.data.timestamp;
|
||||
this.chartInstance.setOption(option, true);
|
||||
const lengthAfter = this.data.timestamp.length;
|
||||
|
||||
// Update the zoom to keep the same range after the update
|
||||
this.chartInstance.dispatchAction({
|
||||
type: 'dataZoom',
|
||||
startValue: startIndex,
|
||||
endValue: endIndex + lengthAfter - lengthBefore,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
// Update the chart
|
||||
const newOption = this.chartInstance.getOption();
|
||||
this.zoomSpan = newOption.dataZoom[0].end - newOption.dataZoom[0].start;
|
||||
this.zoomTimeSpan = this.getTimeRangeFromTimespan(Math.floor(this.data.timestamp[newOption.dataZoom[0].startValue] / 1000), Math.floor(this.data.timestamp[newOption.dataZoom[0].endValue] / 1000));
|
||||
this.isLoading = false;
|
||||
}),
|
||||
catchError(() => {
|
||||
const newOption = this.chartInstance.getOption();
|
||||
this.zoomSpan = newOption.dataZoom[0].end - newOption.dataZoom[0].start;
|
||||
this.zoomTimeSpan = this.getTimeRangeFromTimespan(Math.floor(this.data.timestamp[newOption.dataZoom[0].startValue] / 1000), Math.floor(this.data.timestamp[newOption.dataZoom[0].endValue] / 1000));
|
||||
this.isLoading = false;
|
||||
this.cd.detectChanges();
|
||||
return [];
|
||||
})
|
||||
).subscribe(() => {
|
||||
subscription.unsubscribe();
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
getTimeRangeFromTimespan(from: number, to: number): string {
|
||||
const timespan = to - from;
|
||||
switch (true) {
|
||||
case timespan >= 3600 * 24 * 365 * 4: return 'all';
|
||||
case timespan >= 3600 * 24 * 365 * 3: return '4y';
|
||||
case timespan >= 3600 * 24 * 365 * 2: return '3y';
|
||||
case timespan >= 3600 * 24 * 365: return '2y';
|
||||
case timespan >= 3600 * 24 * 30 * 6: return '1y';
|
||||
case timespan >= 3600 * 24 * 30 * 3: return '6m';
|
||||
case timespan >= 3600 * 24 * 30: return '3m';
|
||||
case timespan >= 3600 * 24 * 7: return '1m';
|
||||
case timespan >= 3600 * 24 * 3: return '1w';
|
||||
case timespan >= 3600 * 24: return '3d';
|
||||
default: return '24h';
|
||||
}
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
const now = new Date();
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = 'var(--active-bg)';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
excludeComponents: ['dataZoom'],
|
||||
}), `block-fees-subsidy-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
tooltipPosition: Position;
|
||||
|
||||
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;
|
||||
searchSubscription: Subscription;
|
||||
|
@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
destroy(): void {
|
||||
if (this.scene) {
|
||||
this.scene.destroy();
|
||||
this.clearUpdateQueue();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
}
|
||||
this.filtersAvailable = filtersAvailable;
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.setup(transactions);
|
||||
this.readyNextFrame = true;
|
||||
this.start();
|
||||
|
@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
enter(transactions: TransactionStripped[], direction: string): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.enter(transactions, direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
|
@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
exit(direction: string): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.exit(direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
|
@ -213,13 +231,67 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.replace(transactions || [], direction, sort, startTime);
|
||||
this.start();
|
||||
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 {
|
||||
// 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) {
|
||||
add = add.filter(tx => !this.scene.txs[tx.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.start();
|
||||
this.lastUpdate = performance.now();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
@ -370,6 +443,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
if (!now) {
|
||||
now = performance.now();
|
||||
}
|
||||
this.applyQueuedUpdates();
|
||||
// skip re-render if there's no change to the scene
|
||||
if (this.scene && this.gl) {
|
||||
/* SET UP SHADER UNIFORMS */
|
||||
|
@ -577,13 +651,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
|
||||
return (tx: TxView) => {
|
||||
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));
|
||||
} else {
|
||||
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
|
||||
}
|
||||
} 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(
|
||||
tx,
|
||||
defaultColors.unmatchedfee,
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class BlockScene {
|
|||
theme: ThemeService;
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 900;
|
||||
animationDuration: number = 1000;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
|
@ -69,7 +69,7 @@ export default class BlockScene {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ export default class BlockScene {
|
|||
removed.forEach(tx => {
|
||||
tx.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
}, (startTime - performance.now()) + this.animationDuration + 1000);
|
||||
|
||||
if (resetLayout) {
|
||||
add.forEach(tx => {
|
||||
|
@ -239,14 +239,14 @@ export default class BlockScene {
|
|||
{ 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 }
|
||||
): void {
|
||||
this.animationDuration = animationDuration || 1000;
|
||||
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
||||
this.configAnimationOffset = animationOffset;
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
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.scene = {
|
||||
|
|
|
@ -177,7 +177,7 @@ export function ageColorFunction(
|
|||
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)))))));
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.block-overview-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(#11131f, 0.95);
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||
color: var(--tooltip-grey);
|
||||
|
@ -30,7 +30,7 @@ th, td {
|
|||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
background-color: #653b9c;
|
||||
box-shadow: #ad7de57f 0px 0px 12px -2px;
|
||||
color: white;
|
||||
animation: acceleratePulse 1s infinite;
|
||||
|
@ -71,7 +71,7 @@ th, td {
|
|||
}
|
||||
|
||||
@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;}
|
||||
100% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
}
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -136,7 +136,12 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||
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([])
|
||||
]);
|
||||
}
|
||||
),
|
||||
|
|
|
@ -345,7 +345,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
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([])
|
||||
]);
|
||||
})
|
||||
)
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
.fee-span {
|
||||
font-size: 11px;
|
||||
margin-bottom: 5px;
|
||||
color: #fff000;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.transaction-count {
|
||||
|
@ -130,7 +130,7 @@
|
|||
height: 0;
|
||||
border-left: 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 {
|
||||
|
|
|
@ -70,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
|
||||
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
|
||||
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
|
||||
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
|
||||
signet: ['var(--signet)', 'var(--signet-alt)'],
|
||||
};
|
||||
|
||||
|
@ -349,7 +350,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
return {
|
||||
left: addLeft + this.blockOffset * index + 'px',
|
||||
background: `repeating-linear-gradient(
|
||||
#2d3348,
|
||||
var(--secondary),
|
||||
var(--secondary) ${greenBackgroundHeight}%,
|
||||
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||
${this.gradientColors[this.network][1]} 100%
|
||||
|
@ -361,7 +362,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
convertStyleForLoadingBlock(style) {
|
||||
return {
|
||||
...style,
|
||||
background: "#2d3348",
|
||||
background: "var(--secondary)",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -370,7 +371,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
return {
|
||||
left: addLeft + (this.blockOffset * index) + 'px',
|
||||
background: "#2d3348",
|
||||
background: "var(--secondary)",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
}
|
||||
|
||||
.time-toggle {
|
||||
color: white;
|
||||
color: var(--fg);
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: -1.8em;
|
||||
|
@ -68,7 +68,7 @@
|
|||
}
|
||||
|
||||
.block-display-toggle {
|
||||
color: white;
|
||||
color: var(--fg);
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: 15.8em;
|
||||
|
|
|
@ -55,7 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
|||
firstValueFrom(this.stateService.chainTip$).then(() => {
|
||||
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 {
|
||||
|
|
|
@ -32,11 +32,12 @@ export class ClockComponent implements OnInit {
|
|||
limitHeight: number;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
liquid: ['#116761', '#183550'],
|
||||
'liquidtestnet': ['#494a4a', '#272e46'],
|
||||
testnet: ['#1d486f', '#183550'],
|
||||
signet: ['#6f1d5d', '#471850'],
|
||||
'': ['var(--mainnet-alt)', 'var(--primary)'],
|
||||
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
|
||||
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
|
||||
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
|
||||
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
|
||||
signet: ['var(--signet)', 'var(--signet-alt)'],
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
@ -99,8 +100,8 @@ export class ClockComponent implements OnInit {
|
|||
|
||||
return {
|
||||
background: `repeating-linear-gradient(
|
||||
#2d3348,
|
||||
#2d3348 ${greenBackgroundHeight}%,
|
||||
var(--secondary),
|
||||
var(--secondary) ${greenBackgroundHeight}%,
|
||||
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||
${this.gradientColors[''][1]} 100%
|
||||
)`,
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
|
||||
<div class="container-xl dashboard-container">
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||
@for (widget of widgets; track widget.component) {
|
||||
@switch (widget.component) {
|
||||
@case ('fees') {
|
||||
<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="card">
|
||||
<div class="card-body less-padding">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('difficulty') {
|
||||
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<app-difficulty></app-difficulty>
|
||||
</div>
|
||||
}
|
||||
@case ('goggles') {
|
||||
<div class="col" [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">
|
||||
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline"><span>Mempool Goggles™</span> : {{ goggleCycle[goggleIndex].name }}</h5>
|
||||
<span> </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>
|
||||
<div class="quick-filter">
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
[filterFlags]="goggleFlags"
|
||||
[filterMode]="goggleMode"
|
||||
[gradientMode]="gradientMode"
|
||||
></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('incoming') {
|
||||
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
<app-incoming-transactions-graph
|
||||
[height]="incomingGraphHeight"
|
||||
[left]="50"
|
||||
[right]="20"
|
||||
[data]="mempoolStats?.weightPerSecond"
|
||||
[windowPreferenceOverride]="'2h'"
|
||||
></app-incoming-transactions-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #mempoolTable let-mempoolInfoData>
|
||||
<div class="mempool-info-data">
|
||||
<div class="item">
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</h5>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
{{ mempoolInfoData.value.memPoolInfo.size | number }} <span i18n="dashboard.txs">TXs</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item bar">
|
||||
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5>
|
||||
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('replacements') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
|
||||
<span> </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>
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
|
||||
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</thead>
|
||||
<tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
|
||||
<tr *ngFor="let replacement of replacements">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-badges">
|
||||
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #replacementsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('blocks') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
|
||||
<span> </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>
|
||||
<table class="table lastest-blocks-table">
|
||||
<thead>
|
||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||
<th class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
|
||||
<tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
|
||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||
<td class="table-cell-size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #blocksSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-height"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-mined"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-transaction-count"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-size"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('transactions') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
|
||||
<table class="table latest-transactions">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
|
||||
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
</thead>
|
||||
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
|
||||
<tr *ngFor="let transaction of transactions; let i = index;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class=""> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #recentTransactionsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('balance') {
|
||||
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
|
||||
<app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget>
|
||||
</div>
|
||||
}
|
||||
@case ('address') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
|
||||
<span> </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>
|
||||
<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>
|
||||
}
|
||||
@case ('addressTransactions') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
||||
<span> </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>
|
||||
<app-address-transactions-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-address-transactions-widget>
|
||||
</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> </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>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="skeleton-loader"></div>
|
||||
</ng-template>
|
||||
<ng-template #loadingbig>
|
||||
<span class="skeleton-loader skeleton-loader-big" ></span>
|
||||
</ng-template>
|
|
@ -0,0 +1,490 @@
|
|||
.dashboard-container {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--title-fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
float: left;
|
||||
width: 350px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: var(--secondary);
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #b58800 !important;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 768px) {
|
||||
height: 415px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 510px;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-info-data {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&.lbtc-pegs-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
margin: 0px auto 20px;
|
||||
display: inline-block;
|
||||
@media (min-width: 485px) {
|
||||
margin: 0px auto 10px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
.bitcoin-color {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
width: 90%;
|
||||
@media (min-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
width: 93%;
|
||||
margin: 0px 5px 20px;
|
||||
@media (min-width: 485px) {
|
||||
max-width: 200px;
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
}
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
display: block;
|
||||
margin: 18px auto 0;
|
||||
}
|
||||
.skeleton-loader-big {
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-transactions {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-satoshis {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fiat {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fees {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.skeleton-loader-transactions {
|
||||
max-width: 250px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: -3px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 15%;
|
||||
}
|
||||
.table-cell-mined {
|
||||
width: 35%;
|
||||
text-align: left;
|
||||
}
|
||||
.table-cell-transaction-count {
|
||||
display: none;
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
display: table-cell;
|
||||
}
|
||||
.table-cell-size {
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lastest-replacements-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-txid {
|
||||
width: 25%;
|
||||
text-align: start;
|
||||
}
|
||||
.table-cell-old-fee {
|
||||
width: 25%;
|
||||
text-align: end;
|
||||
|
||||
@media(max-width: 1080px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.table-cell-new-fee {
|
||||
width: 20%;
|
||||
text-align: end;
|
||||
}
|
||||
.table-cell-badges {
|
||||
width: 23%;
|
||||
padding-right: 0;
|
||||
padding-left: 5px;
|
||||
text-align: end;
|
||||
|
||||
.badge {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-graph {
|
||||
height: 255px;
|
||||
@media (min-width: 768px) {
|
||||
height: 285px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 370px;
|
||||
}
|
||||
}
|
||||
.loadingGraphs{
|
||||
height: 250px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.inc-tx-progress-bar {
|
||||
max-width: 250px;
|
||||
.progress-bar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
&.liquid {
|
||||
height: 124.5px;
|
||||
}
|
||||
}
|
||||
.less-padding {
|
||||
padding: 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.assetIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.asset-title {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.circulating-amount {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 410px;
|
||||
max-width: 410px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-height: 344px;
|
||||
max-width: 344px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
max-height: 410px;
|
||||
max-width: 410px;
|
||||
}
|
||||
}
|
||||
|
||||
.goggle-badge {
|
||||
margin: 6px 5px 8px;
|
||||
background: none;
|
||||
border: solid 2px var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-liquid {
|
||||
background-color: var(--bg);
|
||||
height: 418px;
|
||||
@media (min-width: 992px) {
|
||||
height: 512px;
|
||||
}
|
||||
&.smaller {
|
||||
height: 408px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title-liquid {
|
||||
padding-top: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: var(--title-fg);
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface MempoolInfoData {
|
||||
memPoolInfo: MempoolInfo;
|
||||
vBytesPerSecond: number;
|
||||
progressWidth: string;
|
||||
progressColor: string;
|
||||
}
|
||||
|
||||
interface MempoolStatsData {
|
||||
mempool: OptimizedMempoolStats[];
|
||||
weightPerSecond: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-dashboard',
|
||||
templateUrl: './custom-dashboard.component.html',
|
||||
styleUrls: ['./custom-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
replacements$: Observable<ReplacementInfo[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
mempoolStats$: Observable<MempoolStatsData>;
|
||||
transactionsWeightPerSecondOptions: any;
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
isLoad: boolean = true;
|
||||
filterSubscription: Subscription;
|
||||
mempoolInfoSubscription: Subscription;
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
incomingGraphHeight: number = 300;
|
||||
graphHeight: number = 300;
|
||||
webGlEnabled = true;
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
|
||||
widgets;
|
||||
|
||||
addressSubscription: Subscription;
|
||||
blockTxSubscription: Subscription;
|
||||
addressSummary$: Observable<AddressTxSummary[]>;
|
||||
address: Address;
|
||||
|
||||
goggleResolution = 82;
|
||||
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
|
||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||
];
|
||||
goggleFlags = 0n;
|
||||
goggleMode: FilterMode = 'and';
|
||||
gradientMode: GradientMode = 'age';
|
||||
goggleIndex = 0;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
this.widgets = this.stateService.env.customize?.dashboard.widgets || [];
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.filterSubscription.unsubscribe();
|
||||
this.mempoolInfoSubscription.unsubscribe();
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
if (this.addressSubscription) {
|
||||
this.addressSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingAddress();
|
||||
this.address = null;
|
||||
}
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.seoService.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.websocketService.startTrackRbfSummary();
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||
.pipe(
|
||||
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
|
||||
);
|
||||
|
||||
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
|
||||
const activeFilters = active.filters.sort().join(',');
|
||||
for (const goggle of this.goggleCycle) {
|
||||
if (goggle.mode === active.mode) {
|
||||
const goggleFilters = goggle.filters.sort().join(',');
|
||||
if (goggleFilters === activeFilters) {
|
||||
this.goggleIndex = goggle.index;
|
||||
this.goggleFlags = toFlags(goggle.filters);
|
||||
this.goggleMode = goggle.mode;
|
||||
this.gradientMode = active.gradient;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.goggleCycle.push({
|
||||
index: this.goggleCycle.length,
|
||||
name: 'Custom',
|
||||
mode: active.mode,
|
||||
filters: active.filters,
|
||||
gradient: active.gradient,
|
||||
});
|
||||
this.goggleIndex = this.goggleCycle.length - 1;
|
||||
this.goggleFlags = toFlags(active.filters);
|
||||
this.goggleMode = active.mode;
|
||||
});
|
||||
|
||||
this.mempoolInfoData$ = combineLatest([
|
||||
this.stateService.mempoolInfo$,
|
||||
this.stateService.vbytesPerSecond$
|
||||
]).pipe(
|
||||
map(([mempoolInfo, vbytesPerSecond]) => {
|
||||
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
|
||||
|
||||
let progressColor = 'bg-success';
|
||||
if (vbytesPerSecond > 1667) {
|
||||
progressColor = 'bg-warning';
|
||||
}
|
||||
if (vbytesPerSecond > 3000) {
|
||||
progressColor = 'bg-danger';
|
||||
}
|
||||
|
||||
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
|
||||
let mempoolSizeProgress = 'bg-danger';
|
||||
if (mempoolSizePercentage <= 50) {
|
||||
mempoolSizeProgress = 'bg-success';
|
||||
} else if (mempoolSizePercentage <= 75) {
|
||||
mempoolSizeProgress = 'bg-warning';
|
||||
}
|
||||
|
||||
return {
|
||||
memPoolInfo: mempoolInfo,
|
||||
vBytesPerSecond: vbytesPerSecond,
|
||||
progressWidth: percent + '%',
|
||||
progressColor: progressColor,
|
||||
mempoolSizeProgress: mempoolSizeProgress,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolInfoSubscription = this.mempoolInfoData$.subscribe();
|
||||
|
||||
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((mempoolBlocks) => {
|
||||
const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
|
||||
const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
|
||||
|
||||
return {
|
||||
size: size,
|
||||
blocks: Math.ceil(vsize / this.stateService.blockVSize)
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.transactions$ = this.stateService.transactions$;
|
||||
|
||||
this.blocks$ = this.stateService.blocks$
|
||||
.pipe(
|
||||
tap((blocks) => {
|
||||
this.latestBlockHeight = blocks[0].height;
|
||||
}),
|
||||
switchMap((blocks) => {
|
||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6));
|
||||
})
|
||||
);
|
||||
|
||||
this.replacements$ = this.stateService.rbfLatestSummary$;
|
||||
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
switchMap((mempoolStats) => {
|
||||
return merge(
|
||||
this.stateService.live2Chart$
|
||||
.pipe(
|
||||
scan((acc, stats) => {
|
||||
const now = Date.now() / 1000;
|
||||
const start = now - (2 * 60 * 60);
|
||||
acc.unshift(stats);
|
||||
acc = acc.filter(p => p.added >= start);
|
||||
return acc;
|
||||
}, (mempoolStats || []))
|
||||
),
|
||||
of(mempoolStats)
|
||||
);
|
||||
}),
|
||||
map((mempoolStats) => {
|
||||
if (mempoolStats) {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
|
||||
};
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
getArrayFromNumber(num: number): number[] {
|
||||
return Array.from({ length: num }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
setFilter(index): void {
|
||||
const selected = this.goggleCycle[index];
|
||||
this.stateService.activeGoggles$.next(selected);
|
||||
}
|
||||
|
||||
startAddressSubscription(): void {
|
||||
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
|
||||
let addressString = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.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 = (
|
||||
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(addressString)
|
||||
: this.electrsApiService.getAddress$(addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
return of(null);
|
||||
}),
|
||||
filter((address) => !!address),
|
||||
).subscribe((address: Address) => {
|
||||
this.websocketService.startTrackAddress(address.address);
|
||||
this.address = address;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.addressSummary$ = (
|
||||
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getScriptHashSummary$((addressString.length === 66 ? '21' : '41') + addressString + 'ac')
|
||||
: this.electrsApiService.getAddressSummary$(addressString)).pipe(
|
||||
catchError(e => {
|
||||
return of(null);
|
||||
}),
|
||||
switchMap(initial => this.stateService.blockTransactions$.pipe(
|
||||
startWith(null),
|
||||
scan((summary, tx) => {
|
||||
if (tx && !summary.some(t => t.txid === tx.txid)) {
|
||||
let value = 0;
|
||||
let funded = 0;
|
||||
let fundedCount = 0;
|
||||
let spent = 0;
|
||||
let spentCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptpubkey_address === addressString) {
|
||||
value += vout.value;
|
||||
funded += vout.value;
|
||||
fundedCount++;
|
||||
}
|
||||
}
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === addressString) {
|
||||
value -= vin.prevout?.value;
|
||||
spent += vin.prevout?.value;
|
||||
spentCount++;
|
||||
}
|
||||
}
|
||||
if (this.address && this.address.address === addressString) {
|
||||
this.address.chain_stats.tx_count++;
|
||||
this.address.chain_stats.funded_txo_sum += funded;
|
||||
this.address.chain_stats.funded_txo_count += fundedCount;
|
||||
this.address.chain_stats.spent_txo_sum += spent;
|
||||
this.address.chain_stats.spent_txo_count += spentCount;
|
||||
}
|
||||
summary.unshift({
|
||||
txid: tx.txid,
|
||||
time: tx.status?.block_time,
|
||||
height: tx.status?.block_height,
|
||||
value
|
||||
});
|
||||
}
|
||||
return summary;
|
||||
}, initial)
|
||||
)),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.incomingGraphHeight = 300;
|
||||
this.goggleResolution = 82;
|
||||
this.graphHeight = 400;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.incomingGraphHeight = 215;
|
||||
this.goggleResolution = 80;
|
||||
this.graphHeight = 310;
|
||||
} else {
|
||||
this.incomingGraphHeight = 180;
|
||||
this.goggleResolution = 86;
|
||||
this.graphHeight = 310;
|
||||
}
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
|
@ -119,7 +119,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.difficulty-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(#11131f, 0.95);
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||
color: #b1b1b1;
|
||||
color: var(--tooltip-grey);
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
<svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#105fb0" />
|
||||
<stop offset="100%" stop-color="#9339f4" />
|
||||
<stop offset="0%" stop-color="var(--primary)" />
|
||||
<stop offset="100%" stop-color="var(--mainnet-alt)" />
|
||||
</linearGradient>
|
||||
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#2486eb" />
|
||||
|
|
|
@ -128,7 +128,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
@ -223,7 +224,7 @@
|
|||
height: 100%;
|
||||
}
|
||||
.background {
|
||||
background: linear-gradient(to right, var(--primary), #9339f4);
|
||||
background: linear-gradient(to right, var(--primary), var(--mainnet-alt));
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
|
91
frontend/src/app/components/faucet/faucet.component.html
Normal file
91
frontend/src/app/components/faucet/faucet.component.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<div class="container-xl">
|
||||
|
||||
<div class="title-block justify-content-center">
|
||||
<h1 i18n="testnet4.faucet">Testnet4 Faucet</h1>
|
||||
</div>
|
||||
|
||||
<div class="faucet-container text-center">
|
||||
|
||||
@if (txid) {
|
||||
<div class="alert alert-success w-100 text-truncate">
|
||||
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
|
||||
Sent!
|
||||
<a class="text-primary" [href]="'/testnet4/tx/' + txid">{{ txid }}</a>
|
||||
</div>
|
||||
}
|
||||
@else if (loading) {
|
||||
<p>Loading faucet...</p>
|
||||
<div class="spinner-border text-light"></div>
|
||||
} @else if (!user) {
|
||||
<!-- User not logged in -->
|
||||
<div class="alert alert-mempool d-block text-center w-100">
|
||||
<div class="d-inline align-middle">
|
||||
<span>To limit abuse, </span>
|
||||
<a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">authenticate </a>
|
||||
<span class="mr-2">or</span>
|
||||
</div>
|
||||
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
|
||||
</div>
|
||||
}
|
||||
@else if (error === 'not_available') {
|
||||
<!-- User logged in but not a paid user or did not link its Twitter account -->
|
||||
<div class="alert alert-mempool d-block text-center w-100">
|
||||
<div class="d-inline align-middle">
|
||||
<span class="mb-2 mr-2">To limit abuse</span>
|
||||
</div>
|
||||
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
||||
</div>
|
||||
}
|
||||
|
||||
@else if (error) {
|
||||
<!-- User can request -->
|
||||
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
<form [formGroup]="faucetForm" class="formGroup" (submit)="requestCoins()" [style]="(error || !this.user) ? 'opacity: 0.3; pointer-events: none' : ''">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group mb-0">
|
||||
<div class="input-group input-group-lg">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" i18n="amount-sats">Amount (sats)</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" [class]="{invalid: invalidAmount}" formControlName="satoshis" id="satoshis">
|
||||
<div class="button-group">
|
||||
<button type="button" class="btn btn-secondary" (click)="setAmount(5000)">5k</button>
|
||||
<button type="button" class="btn btn-secondary ml-2" (click)="setAmount(50000)">50k</button>
|
||||
<button type="button" class="btn btn-secondary ml-2" (click)="setAmount(500000)">500k</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-danger text-left" *ngIf="invalidAmount">
|
||||
<div *ngIf="amount?.errors?.['required']">Amount is required</div>
|
||||
<div *ngIf="amount?.errors?.['min']">Minimum is {{ amount?.errors?.['min'].min | number }} tSats</div>
|
||||
<div *ngIf="amount?.errors?.['max']">Maximum is {{ amount?.errors?.['max'].max | number }} tSats</div>
|
||||
</div>
|
||||
<div class="input-group input-group-lg mt-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" i18n="address">Address</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" [class]="{invalid: invalidAddress}" formControlName="address" id="address" placeholder="tb1q...">
|
||||
<button type="submit" class="btn btn-primary submit-button" [disabled]="!faucetForm.valid || !faucetForm.get('address')?.dirty" i18n="testnet4.request-coins">Request Testnet4 Coins</button>
|
||||
</div>
|
||||
<div class="text-danger text-left" *ngIf="invalidAddress">
|
||||
<div *ngIf="address?.errors?.['required']">Address is required</div>
|
||||
<div *ngIf="address?.errors?.['pattern']">Must be a valid testnet4 address</div>
|
||||
<div *ngIf="address?.errors?.['forbiddenAddress']">You cannot use this address</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<!-- Send back coins -->
|
||||
@if (status?.address) {
|
||||
<div class="mt-2 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
52
frontend/src/app/components/faucet/faucet.component.scss
Normal file
52
frontend/src/app/components/faucet/faucet.component.scss
Normal file
|
@ -0,0 +1,52 @@
|
|||
.formGroup {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
row-gap: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.form-control {
|
||||
min-width: 160px;
|
||||
flex-grow: 100;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.submit-button, .button-group, .button-group .btn {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.submit-button:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#satoshis::after {
|
||||
content: 'sats';
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.faucet-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
border-width: 1px;
|
||||
border-color: var(--red);
|
||||
}
|
155
frontend/src/app/components/faucet/faucet.component.ts
Normal file
155
frontend/src/app/components/faucet/faucet.component.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { Subscription } from "rxjs";
|
||||
import { StorageService } from "../../services/storage.service";
|
||||
import { ServicesApiServices } from "../../services/services-api.service";
|
||||
import { getRegex } from "../../shared/regex.utils";
|
||||
import { StateService } from "../../services/state.service";
|
||||
import { WebsocketService } from "../../services/websocket.service";
|
||||
import { AudioService } from "../../services/audio.service";
|
||||
import { HttpErrorResponse } from "@angular/common/http";
|
||||
|
||||
@Component({
|
||||
selector: 'app-faucet',
|
||||
templateUrl: './faucet.component.html',
|
||||
styleUrls: ['./faucet.component.scss']
|
||||
})
|
||||
export class FaucetComponent implements OnInit, OnDestroy {
|
||||
loading = true;
|
||||
error: string = '';
|
||||
user: any = undefined;
|
||||
txid: string = '';
|
||||
|
||||
faucetStatusSubscription: Subscription;
|
||||
status: {
|
||||
min: number; // minimum amount to request at once (in sats)
|
||||
max: number; // maximum amount to request at once
|
||||
address?: string; // faucet address
|
||||
code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon';
|
||||
} | null = null;
|
||||
faucetForm: FormGroup;
|
||||
|
||||
mempoolPositionSubscription: Subscription;
|
||||
confirmationSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private storageService: StorageService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private formBuilder: FormBuilder,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private audioService: AudioService
|
||||
) {
|
||||
this.faucetForm = this.formBuilder.group({
|
||||
'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]],
|
||||
'satoshis': [0, [Validators.required, Validators.min(0), Validators.max(0)]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stateService.markBlock$.next({});
|
||||
this.websocketService.stopTrackingTransaction();
|
||||
if (this.mempoolPositionSubscription) {
|
||||
this.mempoolPositionSubscription.unsubscribe();
|
||||
}
|
||||
if (this.confirmationSubscription) {
|
||||
this.confirmationSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.user = this.storageService.getAuth()?.user ?? null;
|
||||
if (!this.user) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup form
|
||||
this.faucetStatusSubscription = this.servicesApiService.getFaucetStatus$().subscribe({
|
||||
next: (status) => {
|
||||
if (!status) {
|
||||
this.error = 'internal_server_error';
|
||||
return;
|
||||
}
|
||||
this.status = status;
|
||||
|
||||
const notFaucetAddressValidator = (faucetAddress: string): ValidatorFn => {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const forbidden = control.value === faucetAddress;
|
||||
return forbidden ? { forbiddenAddress: { value: control.value } } : null;
|
||||
};
|
||||
}
|
||||
this.faucetForm = this.formBuilder.group({
|
||||
'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), notFaucetAddressValidator(this.status.address)]],
|
||||
'satoshis': [this.status.min, [Validators.required, Validators.min(this.status.min), Validators.max(this.status.max)]]
|
||||
});
|
||||
|
||||
if (this.status.code !== 'ok') {
|
||||
this.error = this.status.code;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.cd.markForCheck();
|
||||
},
|
||||
error: (response) => {
|
||||
this.loading = false;
|
||||
this.error = response.error;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
// Track transaction
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
||||
if (txPosition && txPosition.txid === this.txid) {
|
||||
this.stateService.markBlock$.next({
|
||||
txid: txPosition.txid,
|
||||
mempoolPosition: txPosition.position,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.confirmationSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
if (txConfirmed && txConfirmed === this.txid) {
|
||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestCoins(): void {
|
||||
this.error = null;
|
||||
this.txid = '';
|
||||
this.stateService.markBlock$.next({});
|
||||
this.servicesApiService.requestTestnet4Coins$(this.faucetForm.get('address')?.value, parseInt(this.faucetForm.get('satoshis')?.value))
|
||||
.subscribe({
|
||||
next: ((response) => {
|
||||
this.txid = response.txid;
|
||||
this.websocketService.startTrackTransaction(this.txid);
|
||||
this.audioService.playSound('cha-ching');
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
error: (response: HttpErrorResponse) => {
|
||||
this.error = response.error;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setAmount(value: number): void {
|
||||
if (this.faucetForm) {
|
||||
this.faucetForm.get('satoshis').setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
get amount() { return this.faucetForm.get('satoshis')!; }
|
||||
get invalidAmount() {
|
||||
const amount = this.faucetForm.get('satoshis')!;
|
||||
return amount?.invalid && (amount.dirty || amount.touched)
|
||||
}
|
||||
|
||||
get address() { return this.faucetForm.get('address')!; }
|
||||
get invalidAddress() {
|
||||
const address = this.faucetForm.get('address')!;
|
||||
return address?.invalid && (address.dirty || address.touched)
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: background-color 1s;
|
||||
color: var(--color-fg);
|
||||
color: #fff;
|
||||
&.priority {
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
width: 100%;
|
||||
|
|
|
@ -16,8 +16,8 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
|
|||
isLoading$: Observable<boolean>;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
themeSubscription: Subscription;
|
||||
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
|
||||
noPriority = '#2e324e';
|
||||
gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))';
|
||||
noPriority = 'var(--skeleton-bg)';
|
||||
fees: Recommendedfees;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu">
|
||||
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING || stateService.env.ACCELERATOR" class="mb-3 d-flex menu" [style]="{'flex-wrap': flexWrap ? 'wrap' : ''}">
|
||||
|
||||
<a routerLinkActive="active" class="btn btn-primary" [class]="padding"
|
||||
<a routerLinkActive="active" class="btn btn-primary w-33"
|
||||
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
|
||||
|
||||
<div ngbDropdown [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<div ngbDropdown class="w-33" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
|
||||
|
@ -17,6 +17,8 @@
|
|||
i18n="mining.block-fee-rates">Block Fee Rates</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"
|
||||
i18n="mining.block-fees">Block Fees</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/block-fees-subsidy' | relativeUrl]"
|
||||
i18n="mining.block-fees">Block Fees Vs Subsidy</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"
|
||||
i18n="mining.block-rewards">Block Rewards</a>
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
|
@ -26,7 +28,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbDropdown [class]="padding" *ngIf="stateService.env.LIGHTNING">
|
||||
<div ngbDropdown class="w-33" *ngIf="stateService.networkSupportsLightning()">
|
||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="lightning">Lightning</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"
|
||||
|
@ -43,6 +45,14 @@
|
|||
i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbDropdown class="w-33" *ngIf="stateService.env.ACCELERATOR">
|
||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="accelerator.accelerations">Accelerations</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]"
|
||||
i18n="accelerator.acceleration-fees">Acceleration Fees</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
flex-grow: 1;
|
||||
padding: 0 35px;
|
||||
@media (min-width: 576px) {
|
||||
max-width: 400px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
|
@ -11,5 +11,6 @@
|
|||
&.last-child {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import { WebsocketService } from '../../services/websocket.service';
|
|||
styleUrls: ['./graphs.component.scss'],
|
||||
})
|
||||
export class GraphsComponent implements OnInit {
|
||||
padding = 'w-50';
|
||||
flexWrap = false;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
|
@ -18,8 +18,8 @@ export class GraphsComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
if (this.stateService.env.MINING_DASHBOARD === true && this.stateService.env.LIGHTNING === true) {
|
||||
this.padding = 'w-33';
|
||||
if (this.stateService.env.ACCELERATOR === true && (this.stateService.env.MINING_DASHBOARD === true || this.stateService.env.LIGHTNING === true)) {
|
||||
this.flexWrap = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
color: var(--fg);
|
||||
opacity: var(--opacity);
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue