Compare commits

..

1 Commits

Author SHA1 Message Date
Mononaut
fa86f97ae8 handle SIGHUP exit code 2023-11-13 07:33:53 +00:00
144 changed files with 65774 additions and 101782 deletions

View File

@@ -52,7 +52,6 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000", "REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000, "RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000, "REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000, "FALLBACK_TIMEOUT": 5000,
@@ -133,11 +132,6 @@
"BISQ_URL": "https://bisq.markets/api", "BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api" "BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
}, },
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": { "REPLICATION": {
"ENABLED": false, "ENABLED": false,
"AUDIT": false, "AUDIT": false,

View File

@@ -53,7 +53,6 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 888, "RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000, "REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000, "FALLBACK_TIMEOUT": 5000,
@@ -141,7 +140,6 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": false, "ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock", "UNIX_SOCKET_PATH": "/tmp/redis.sock"
"BATCH_QUERY_BASE_SIZE": 5000
} }
} }

View File

@@ -55,7 +55,6 @@ describe('Mempool Backend Config', () => {
expect(config.ESPLORA).toStrictEqual({ expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000', REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null, UNIX_SOCKET_PATH: null,
BATCH_QUERY_BASE_SIZE: 1000,
RETRY_UNIX_SOCKET_AFTER: 30000, RETRY_UNIX_SOCKET_AFTER: 30000,
REQUEST_TIMEOUT: 10000, REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000, FALLBACK_TIMEOUT: 5000,
@@ -145,8 +144,7 @@ describe('Mempool Backend Config', () => {
expect(config.REDIS).toStrictEqual({ expect(config.REDIS).toStrictEqual({
ENABLED: false, ENABLED: false,
UNIX_SOCKET_PATH: '', UNIX_SOCKET_PATH: ''
BATCH_QUERY_BASE_SIZE: 5000,
}); });
}); });
}); });

View File

@@ -9,7 +9,7 @@ class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
@@ -144,12 +144,7 @@ class Audit {
const numCensored = Object.keys(isCensored).length; const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx const numMatches = matches.length - 1; // adjust for coinbase tx
let score = 0; const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
if (numMatches <= 0 && numCensored <= 0) {
score = 1;
} else if (numMatches > 0) {
score = (numMatches / (numMatches + numCensored));
}
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {

View File

@@ -5,7 +5,7 @@ export interface AbstractBitcoinApi {
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number); $getAllMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>; $getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>; $getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>; $getBlockHashTip(): Promise<string>;

View File

@@ -77,7 +77,7 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
} }
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> { $getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
} }

View File

@@ -573,9 +573,7 @@ class BitcoinRoutes {
} }
try { try {
// electrum expects scripthashes in little-endian const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
@@ -592,13 +590,11 @@ class BitcoinRoutes {
} }
try { try {
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
let lastTxId: string = ''; let lastTxId: string = '';
if (req.query.after_txid && typeof req.query.after_txid === 'string') { if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid; lastTxId = req.query.after_txid;
} }
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId); const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {

View File

@@ -8,9 +8,8 @@ import logger from '../../logger';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
rtt: number, rtt: number
failures: number, failures: number,
latestHeight?: number,
socket?: boolean, socket?: boolean,
outOfSync?: boolean, outOfSync?: boolean,
unreachable?: boolean, unreachable?: boolean,
@@ -93,7 +92,6 @@ class FailoverRouter {
host.rtts.unshift(rtt); host.rtts.unshift(rtt);
host.rtts.slice(0, 5); host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (maxHeight - height > 2)) { if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true; host.outOfSync = true;
} else { } else {
@@ -101,23 +99,22 @@ class FailoverRouter {
} }
host.unreachable = false; host.unreachable = false;
} else { } else {
host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
} }
} }
this.sortHosts(); this.sortHosts();
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`); logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative // switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) { if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) { if (this.activeHost.unreachable) {
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`); logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
} else if (this.activeHost.outOfSync) { } else if (this.activeHost.outOfSync) {
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`); logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
} else { } else {
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`); logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
} }
this.electHost(); this.electHost();
} }
@@ -125,11 +122,6 @@ class FailoverRouter {
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval); this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
} }
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
// sort hosts by connection quality, and update default fallback // sort hosts by connection quality, and update default fallback
private sortHosts(): void { private sortHosts(): void {
// sort by connection quality // sort by connection quality
@@ -164,7 +156,7 @@ class FailoverRouter {
private addFailure(host: FailoverHost): FailoverHost { private addFailure(host: FailoverHost): FailoverHost {
host.failures++; host.failures++;
if (host.failures > 5 && this.multihost) { if (host.failures > 5 && this.multihost) {
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`); logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
this.electHost(); this.electHost();
return this.activeHost; return this.activeHost;
} else { } else {
@@ -233,8 +225,8 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json'); return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
} }
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> { async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null); return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {

View File

@@ -761,13 +761,8 @@ class Blocks {
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`); this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
if (!fastForwarded) { if (!fastForwarded) {
let lastestPriceId; const lastestPriceId = await PricesRepository.$getLatestPriceId();
try { this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
}
if (priceUpdater.historyInserted === true && lastestPriceId !== null) { if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{ await blocksRepository.$saveBlockPrices([{
height: blockExtended.height, height: blockExtended.height,
@@ -776,7 +771,9 @@ class Blocks {
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else { } else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
indexer.scheduleSingleTask('blocksPrices', 10000); setTimeout(() => {
indexer.runSingleTask('blocksPrices');
}, 10000);
} }
// Save blocks summary for visualization if it's enabled // Save blocks summary for visualization if it's enabled

View File

@@ -44,13 +44,9 @@ export enum FeatureBits {
KeysendOptional = 55, KeysendOptional = 55,
ScriptEnforcedLeaseRequired = 2022, ScriptEnforcedLeaseRequired = 2022,
ScriptEnforcedLeaseOptional = 2023, ScriptEnforcedLeaseOptional = 2023,
SimpleTaprootChannelsRequiredFinal = 80,
SimpleTaprootChannelsOptionalFinal = 81,
SimpleTaprootChannelsRequiredStaging = 180,
SimpleTaprootChannelsOptionalStaging = 181,
MaxBolt11Feature = 5114, MaxBolt11Feature = 5114,
}; };
export const FeaturesMap = new Map<FeatureBits, string>([ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'], [FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'], [FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
@@ -89,10 +85,6 @@ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.ZeroConfOptional, 'zero-conf'], [FeatureBits.ZeroConfOptional, 'zero-conf'],
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
]); ]);
/** /**

View File

@@ -126,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100); loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) { while (!done) {
try { try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE); const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
if (result) { if (result) {
for (const tx of result) { for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -235,7 +235,7 @@ class Mempool {
if (!loaded) { if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]); const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE; const sliceLength = 10000;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength); const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false); const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);

View File

@@ -480,15 +480,14 @@ class RbfCache {
}; };
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
let processedCount = 0; const sliceLength = 250;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength); const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
processedCount += slice.length;
try { try {
const txs = await bitcoinApi.$getRawTransactions(slice); const txs = await bitcoinApi.$getRawTransactions(slice);
logger.debug(`fetched ${slice.length} cached rbf transactions`);
processTxs(txs); processTxs(txs);
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`); logger.debug(`processed ${slice.length} cached rbf transactions`);
} catch (err) { } catch (err) {
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`); logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
} }

View File

@@ -122,9 +122,8 @@ class RedisCache {
async $removeTransactions(transactions: string[]) { async $removeTransactions(transactions: string[]) {
try { try {
await this.$ensureConnected(); await this.$ensureConnected();
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE; for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) { const slice = transactions.slice(i * 10000, (i + 1) * 10000);
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
} }

View File

@@ -43,7 +43,6 @@ interface IConfig {
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null; UNIX_SOCKET_PATH: string | void | null;
BATCH_QUERY_BASE_SIZE: number;
RETRY_UNIX_SOCKET_AFTER: number; RETRY_UNIX_SOCKET_AFTER: number;
REQUEST_TIMEOUT: number; REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number; FALLBACK_TIMEOUT: number;
@@ -152,7 +151,6 @@ interface IConfig {
REDIS: { REDIS: {
ENABLED: boolean; ENABLED: boolean;
UNIX_SOCKET_PATH: string; UNIX_SOCKET_PATH: string;
BATCH_QUERY_BASE_SIZE: number;
}, },
} }
@@ -197,7 +195,6 @@ const defaults: IConfig = {
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null, 'UNIX_SOCKET_PATH': null,
'BATCH_QUERY_BASE_SIZE': 1000,
'RETRY_UNIX_SOCKET_AFTER': 30000, 'RETRY_UNIX_SOCKET_AFTER': 30000,
'REQUEST_TIMEOUT': 10000, 'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000, 'FALLBACK_TIMEOUT': 5000,
@@ -306,7 +303,6 @@ const defaults: IConfig = {
'REDIS': { 'REDIS': {
'ENABLED': false, 'ENABLED': false,
'UNIX_SOCKET_PATH': '', 'UNIX_SOCKET_PATH': '',
'BATCH_QUERY_BASE_SIZE': 5000,
}, },
}; };

View File

@@ -2,10 +2,8 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import config from './config'; import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise'; import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger'; import logger from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql'; import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';
class DB { class DB {
constructor() { constructor() {
@@ -34,7 +32,7 @@ import { execSync } from 'child_process';
} }
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]> OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{ {
this.checkDBFlag(); this.checkDBFlag();
let hardTimeout; let hardTimeout;
@@ -56,38 +54,19 @@ import { execSync } from 'child_process';
}).then(result => { }).then(result => {
resolve(result); resolve(result);
}).catch(error => { }).catch(error => {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
reject(error); reject(error);
}).finally(() => { }).finally(() => {
clearTimeout(timer); clearTimeout(timer);
}); });
}); });
} else { } else {
try { const pool = await this.getPool();
const pool = await this.getPool(); return pool.query(query, params);
return pool.query(query, params);
} catch (e) {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
throw e;
}
}
}
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
try {
await connection.rollback();
await connection.release();
} catch (e) {
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
} }
} }
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]> OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
{ {
const pool = await this.getPool(); const pool = await this.getPool();
const connection = await pool.getConnection(); const connection = await pool.getConnection();
@@ -96,7 +75,7 @@ import { execSync } from 'child_process';
const results: [T, FieldPacket[]][] = []; const results: [T, FieldPacket[]][] = [];
for (const query of queries) { for (const query of queries) {
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]]; const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
results.push(result); results.push(result);
} }
@@ -104,8 +83,9 @@ import { execSync } from 'child_process';
return results; return results;
} catch (e) { } catch (e) {
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e)); logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
this.$rollbackAtomic(connection); connection.rollback();
connection.release();
throw e; throw e;
} finally { } finally {
connection.release(); connection.release();
@@ -125,43 +105,26 @@ import { execSync } from 'child_process';
public getPidLock(): boolean { public getPidLock(): boolean {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`); const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
this.enforcePidLock(filePath);
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
private enforcePidLock(filePath: string): void {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8')); const pid = fs.readFileSync(filePath).toString();
if (pid === process.pid) { if (pid !== `${process.pid}`) {
logger.warn('PID file already exists for this process'); const msg = `Already running on PID ${pid} (or pid file '${filePath}' is stale)`;
return;
}
let cmd;
try {
cmd = execSync(`ps -p ${pid} -o args=`);
} catch (e) {
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
return;
}
if (cmd && cmd.toString()?.includes('node')) {
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
logger.err(msg); logger.err(msg);
throw new Error(msg); throw new Error(msg);
} else { } else {
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`); return true;
} }
} else {
fs.writeFileSync(filePath, `${process.pid}`);
return true;
} }
} }
public releasePidLock(): void { public releasePidLock(): void {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`); const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8')); const pid = fs.readFileSync(filePath).toString();
// only release our own pid file if (pid === `${process.pid}`) {
if (pid === process.pid) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
} }

View File

@@ -92,15 +92,9 @@ class Server {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
// Register cleanup listeners for exit events // Register cleanup listeners for exit events
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => { ['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => {
process.on(event, () => { this.onExit(event); }); process.on(event, () => { this.onExit(event); });
}); });
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks(); bitcoinApi.startHealthChecks();
@@ -206,7 +200,7 @@ class Server {
} }
const newMempool = await bitcoinApi.$getRawMempool(); const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks(); const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
if (numHandledBlocks === 0) { if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate); await memPool.$updateMempool(newMempool, pollRate);
} }
@@ -320,18 +314,14 @@ class Server {
} }
} }
onExit(exitEvent, code = 0): void { onExit(exitEvent): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
DB.releasePidLock(); DB.releasePidLock();
} }
process.exit(code); process.exit(0);
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
} }
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@@ -15,18 +15,11 @@ export interface CoreIndex {
best_block_height: number; best_block_height: number;
} }
type TaskName = 'blocksPrices' | 'coinStatsIndex';
class Indexer { class Indexer {
private runIndexer = true; runIndexer = true;
private indexerRunning = false; indexerRunning = false;
private tasksRunning: { [key in TaskName]?: boolean; } = {}; tasksRunning: string[] = [];
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {}; coreIndexes: CoreIndex[] = [];
private coreIndexes: CoreIndex[] = [];
public indexerIsRunning(): boolean {
return this.indexerRunning;
}
/** /**
* Check which core index is available for indexing * Check which core index is available for indexing
@@ -76,69 +69,33 @@ class Indexer {
} }
} }
/** public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
* schedules a single task to run in `timeout` ms if (!Common.indexingEnabled()) {
* only one task of each type may be scheduled
*
* @param {TaskName} task - the type of task
* @param {number} timeout - delay in ms
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
*/
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
if (this.tasksScheduled[task]) {
if (!replace) { //throttle
return;
} else { // debounce
clearTimeout(this.tasksScheduled[task]);
}
}
this.tasksScheduled[task] = setTimeout(async () => {
try {
await this.runSingleTask(task);
} catch (e) {
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
} finally {
clearTimeout(this.tasksScheduled[task]);
}
}, timeout);
}
/**
* Runs a single task immediately
*
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
*/
public async runSingleTask(task: TaskName): Promise<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
return; return;
} }
this.tasksRunning[task] = true;
switch (task) { if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
case 'blocksPrices': { this.tasksRunning.push(task);
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { const lastestPriceId = await PricesRepository.$getLatestPriceId();
let lastestPriceId; if (priceUpdater.historyInserted === false || lastestPriceId === null) {
try { logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
lastestPriceId = await PricesRepository.$getLatestPriceId(); setTimeout(() => {
} catch (e) { this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e)); this.runSingleTask('blocksPrices');
} if (priceUpdater.historyInserted === false || lastestPriceId === null) { }, 10000);
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining); } else {
this.scheduleSingleTask(task, 10000); logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
} else { await mining.$indexBlockPrices();
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
await mining.$indexBlockPrices(); }
}
}
} break;
case 'coinStatsIndex': {
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
} break;
} }
this.tasksRunning[task] = false; if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
}
} }
public async $run(): Promise<void> { public async $run(): Promise<void> {

View File

@@ -157,6 +157,4 @@ class Logger {
} }
} }
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
export default new Logger(); export default new Logger();

View File

@@ -14,7 +14,7 @@ class NodesSocketsRepository {
await DB.query(` await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type) INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?) VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network], 'silent'); `, [socket.publicKey, socket.addr, socket.network]);
} catch (e: any) { } catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -79,7 +79,7 @@ class ForensicsService {
} }
let progress = 0; let progress = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10); const sliceLength = 1000;
// process batches of 1000 channels // process batches of 1000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

View File

@@ -290,7 +290,7 @@ class NetworkSyncService {
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]); const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2); const sliceLength = 5000;
// process batches of 5000 channels // process batches of 5000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
Signed: ncois

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 15, 2023.
Signed: shubhamkmr04

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
Signed starius

View File

@@ -53,7 +53,6 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__, "REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__, "FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
@@ -147,7 +146,6 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
} }
} }

View File

@@ -54,7 +54,6 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"} __ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000} __ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000} __ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
@@ -149,7 +148,6 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -203,7 +201,6 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
@@ -291,6 +288,5 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
# REDIS # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@@ -31,10 +31,10 @@
"bootstrap": "~4.6.2", "bootstrap": "~4.6.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"cypress": "^13.6.0",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.4.3", "echarts": "~5.4.3",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"mock-socket": "~9.3.1",
"ngx-echarts": "~16.2.0", "ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
@@ -59,7 +59,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.6.0", "cypress": "^13.5.0",
"cypress-fail-on-console-error": "~5.0.0", "cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",
@@ -7148,9 +7148,9 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.6.0", "version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", "integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -22096,9 +22096,9 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "13.6.0", "version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", "integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.0",

View File

@@ -110,7 +110,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.6.0", "cypress": "^13.5.0",
"cypress-fail-on-console-error": "~5.0.0", "cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy' import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component'; import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component'; import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
@@ -125,10 +124,6 @@ let routes: Routes = [
path: 'view/mempool-block/:index', path: 'view/mempool-block/:index',
component: MempoolBlockViewComponent, component: MempoolBlockViewComponent,
}, },
{
path: 'view/blocks',
component: EightBlocksComponent,
},
{ {
path: 'status', path: 'status',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`); this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`); this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$() this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -32,22 +32,11 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null"> <track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video> </video>
<ng-container *ngIf="officialMempoolSpace"> <ng-container *ngIf="false && officialMempoolSpace">
<div id="become-sponsor-container"> <h3 class="mt-5">Sponsor the project</h3>
<div class="become-sponsor community"> <div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p> <a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Become a Community Sponsor</a> <a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div>
</div> </div>
</ng-container> </ng-container>
@@ -204,7 +193,7 @@
<ng-container> <ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> <ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</ng-container> </ng-container>
@@ -216,7 +205,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> <ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</div> </div>
@@ -310,9 +299,9 @@
<img class="image" src="/resources/profile/blixt.png" /> <img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span> <span>Blixt</span>
</a> </a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> <a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
<img class="image" src="/resources/profile/zeus.png" /> <img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span> <span>Zeus</span>
</a> </a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet"> <a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" /> <img class="image" src="/resources/profile/marina.svg" />

View File

@@ -246,49 +246,3 @@
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
#become-sponsor-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 20px;
margin: 68px auto;
}
.become-sponsor {
background-color: #1d1f31;
border-radius: 16px;
padding: 12px 20px;
width: 400px;
padding: 40px 20px;
}
.become-sponsor a {
margin-top: 10px;
}
#become-sponsor-container .btn {
margin-bottom: 24px;
}
#become-sponsor-container .ng-fa-icon {
color: #2ecc71;
margin-right: 5px;
}
#become-sponsor-container .sponsor-feature {
text-align: left;
width: 250px;
margin: 12px auto;
white-space: nowrap;
}
@media (max-width: 992px) {
#become-sponsor-container {
flex-wrap: wrap;
}
}

View File

@@ -2,6 +2,7 @@
height: 100%; height: 100%;
min-width: 120px; min-width: 120px;
width: 120px; width: 120px;
max-height: 90vh;
margin-left: 4em; margin-left: 4em;
margin-right: 1.5em; margin-right: 1.5em;
padding-bottom: 63px; padding-bottom: 63px;
@@ -17,7 +18,6 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
min-height: 30px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View File

@@ -58,15 +58,13 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
fee: option.fee, fee: option.fee,
} }
}); });
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) { bars.push({
bars.push({ rate: this.estimate.targetFeeRate,
rate: this.estimate.targetFeeRate, style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), class: 'target',
class: 'target', label: 'next block',
label: 'next block', fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee });
});
}
bars.push({ bars.push({
rate: baseRate, rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0), style: this.getStyle(baseRate, maxRate, 0),

View File

@@ -1,16 +1,14 @@
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="showSuccess"> <div class="row" *ngIf="showSuccess">
<div class="col"> <div class="col" id="successAlert">
<div class="alert alert-success"> <div class="alert alert-success">
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration. Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
</div> </div>
</div> </div>
</div> </div>
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="error"> <div class="row" *ngIf="error">
<div class="col"> <div class="col" id="mempoolError">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error> <app-mempool-error [error]="error"></app-mempool-error>
</div> </div>
</div> </div>
@@ -39,10 +37,10 @@
<td class="item"> <td class="item">
Virtual size Virtual size
</td> </td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> <td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr> </tr>
<tr class="info"> <tr class="info">
<td class="info" colspan=3> <td class="info">
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> <i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td> </td>
</tr> </tr>
@@ -50,12 +48,12 @@
<td class="item"> <td class="item">
In-band fees In-band fees
</td> </td>
<td style="text-align: end;"> <td class="units">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> <i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td> </td>
</tr> </tr>
@@ -76,8 +74,8 @@
<div class="d-flex mb-0"> <div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions"> <ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)"> <button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span> <span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> <span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button> </button>
</ng-container> </ng-container>
</div> </div>
@@ -89,15 +87,23 @@
<h5>Acceleration summary</h5> <h5>Acceleration summary</h5>
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="table-toggle btn-group btn-group-toggle">
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
<span>Estimated cost</span>
</div>
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
<span>Maximum cost</span>
</div>
</div>
<table class="table table-borderless table-border table-dark table-accelerator"> <table class="table table-borderless table-border table-dark table-accelerator">
<tbody> <tbody>
<!-- ESTIMATED FEE --> <!-- ESTIMATED FEE -->
<ng-container> <ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first"> <tr class="group-first">
<td class="item"> <td class="item">
Next block market rate Next block market rate
</td> </td>
<td class="amt" style="font-size: 16px"> <td class="amt" style="font-size: 20px">
{{ estimate.targetFeeRate | number : '1.0-0' }} {{ estimate.targetFeeRate | number : '1.0-0' }}
</td> </td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
@@ -110,8 +116,34 @@
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> <span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- USER MAX BID -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
Your maximum
</td>
<td class="amt" style="width: 45%; font-size: 20px">
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>The maximum extra transaction fee you could pay</small></i>
</td>
<td class="amt">
<span>
{{ userBid | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
@@ -130,11 +162,11 @@
+{{ estimate.mempoolBaseFee | number }} +{{ estimate.mempoolBaseFee | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info"> <td class="info">
<i><small>Transaction vsize fee</small></i> <i><small>Transaction vsize fee</small></i>
</td> </td>
@@ -142,14 +174,14 @@
+{{ estimate.vsizeFee | number }} +{{ estimate.vsizeFee | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<!-- NEXT BLOCK ESTIMATE --> <!-- NEXT BLOCK ESTIMATE -->
<ng-container> <ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;"> <tr class="group-first">
<td class="item"> <td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b> <b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td> </td>
@@ -159,19 +191,19 @@
</span> </span>
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> <span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td> </td>
</tr> </tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i> <i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
<!-- MAX COST --> <!-- MAX COST -->
<ng-container> <ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first"> <tr class="group-first">
<td class="item"> <td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b> <b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
@@ -182,21 +214,21 @@
</span> </span>
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"> <span class="fiat">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> <app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span> </span>
</td> </td>
</tr> </tr>
<tr class="info group-last"> <tr class="info group-last">
<td class="info" colspan=3> <td class="info">
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i> <i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
<!-- USER BALANCE --> <!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost"> <ng-container *ngIf="estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey"> <tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"> <td class="item">
Available balance Available balance
@@ -205,24 +237,13 @@
{{ estimate.userBalance | number }} {{ estimate.userBalance | number }}
</td> </td>
<td class="units"> <td class="units">
<span class="symbol" i18n="shared.sats">sats</span> <span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat ml-1"> <span class="fiat">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span> </span>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="!isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
</td>
</tr>
</ng-container>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -8,6 +8,9 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.fee {
font-size: 1.2em;
}
.rate { .rate {
font-size: 0.9em; font-size: 0.9em;
.symbol { .symbol {
@@ -25,10 +28,7 @@
.feerate.active { .feerate.active {
background-color: #105fb0 !important; background-color: #105fb0 !important;
opacity: 1; opacity: 1;
border: 1px solid #007fff !important; border: 1px solid white !important;
}
.feerate:focus {
box-shadow: none !important;
} }
.estimateDisabled { .estimateDisabled {
@@ -41,26 +41,10 @@
margin-top: 0.5em; margin-top: 0.5em;
} }
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator { .table-accelerator {
tr { tr {
text-wrap: wrap;
td { td {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
@@ -84,7 +68,6 @@
} }
&.info { &.info {
color: #6c757d; color: #6c757d;
white-space: initial;
} }
&.amt { &.amt {
text-align: right; text-align: right;
@@ -93,9 +76,6 @@
&.units { &.units {
padding-left: 0.2em; padding-left: 0.2em;
white-space: nowrap; white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
} }
} }
} }
@@ -105,8 +85,4 @@
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
margin-top: 1em; margin-top: 1em;
}
.item {
white-space: initial;
} }

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs'; import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
@@ -55,14 +55,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxCost = 0; maxCost = 0;
userBid = 0; userBid = 0;
selectFeeRateIndex = 1; selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98; isMobile: boolean = window.innerWidth <= 767.98;
maxRateOptions: RateOption[] = []; maxRateOptions: RateOption[] = [];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private storageService: StorageService, private storageService: StorageService
private cd: ChangeDetectorRef
) { } ) { }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -73,7 +73,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) { if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start'); this.scrollToPreview('acceleratePreviewAnchor', 'center');
} }
} }
@@ -126,7 +126,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) { if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'start'); this.scrollToPreview('acceleratePreviewAnchor', 'center');
} }
} }
}), }),
@@ -162,14 +162,13 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
scrollToPreview(id: string, position: ScrollLogicalPosition) { scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id); const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) { if (acceleratePreviewAnchor) {
this.cd.markForCheck();
acceleratePreviewAnchor.scrollIntoView({ acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
inline: position, inline: position,
block: position, block: position,
}); });
} }
} }
/** /**
* Send acceleration request * Send acceleration request
@@ -188,11 +187,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
}, },
error: (response) => { error: (response) => {
if (response.status === 403 && response.error === 'not_available') { this.error = response.error;
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');
} }
}); });

View File

@@ -1,4 +1,4 @@
<header class="sticky-header"> <header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;"> <a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -1,11 +1,3 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active { li.nav-item.active {
background-color: #653b9c; background-color: #653b9c;
} }

View File

@@ -20,8 +20,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() blockLimit: number; @Input() blockLimit: number;
@Input() orientation = 'left'; @Input() orientation = 'left';
@Input() flip = true; @Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false; @Input() disableSpinner = false;
@Input() mirrorTxid: string | void; @Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false; @Input() unavailable: boolean = false;
@@ -143,9 +141,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
if (this.scene) { if (this.scene) {
this.scene.replace(transactions || [], direction, sort, startTime); this.scene.replace(transactions || [], direction, sort);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
} }
@@ -228,7 +226,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else { } else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset }); highlighting: this.auditHighlighting });
this.start(); this.start();
} }
} }

View File

@@ -9,9 +9,6 @@ export default class BlockScene {
txs: { [key: string]: TxView }; txs: { [key: string]: TxView };
orientation: string; orientation: string;
flip: boolean; flip: boolean;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean; highlightingEnabled: boolean;
width: number; width: number;
height: number; height: number;
@@ -26,11 +23,11 @@ export default class BlockScene {
animateUntil = 0; animateUntil = 0;
dirty: boolean; dirty: boolean;
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) { ) {
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }); this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
} }
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@@ -39,7 +36,6 @@ export default class BlockScene {
this.gridSize = this.width / this.gridWidth; this.gridSize = this.width / this.gridWidth;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5)); this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitWidth = this.gridSize - (this.unitPadding * 2); this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
@@ -94,8 +90,8 @@ export default class BlockScene {
} }
// Animate new block entering scene // Animate new block entering scene
enter(txs: TransactionStripped[], direction, startTime?: number) { enter(txs: TransactionStripped[], direction) {
this.replace(txs, direction, false, startTime); this.replace(txs, direction);
} }
// Animate block leaving scene // Animate block leaving scene
@@ -112,7 +108,8 @@ export default class BlockScene {
} }
// Reset layout and replace with new set of transactions // Reset layout and replace with new set of transactions
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void { replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
const startTime = performance.now();
const nextIds = {}; const nextIds = {};
const remove = []; const remove = [];
txs.forEach(tx => { txs.forEach(tx => {
@@ -136,7 +133,7 @@ export default class BlockScene {
removed.forEach(tx => { removed.forEach(tx => {
tx.destroy(); tx.destroy();
}); });
}, (startTime - performance.now()) + this.animationDuration + 1000); }, 1000);
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@@ -150,7 +147,7 @@ export default class BlockScene {
}); });
} }
this.updateAll(startTime, 50, direction); this.updateAll(startTime, 200, direction);
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
@@ -217,13 +214,10 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
} }
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void { ): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation; this.orientation = orientation;
this.flip = flip; this.flip = flip;
this.vertexArray = vertexArray; this.vertexArray = vertexArray;
@@ -267,8 +261,8 @@ export default class BlockScene {
this.applyTxUpdate(tx, { this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)), x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)), y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
s: tx.screenPosition.s s: tx.screenPosition.s
}, },
color: txColor, color: txColor,
@@ -281,7 +275,7 @@ export default class BlockScene {
position: tx.screenPosition, position: tx.screenPosition,
color: txColor color: txColor
}, },
duration: animate ? this.animationDuration : 1, duration: animate ? 1000 : 1,
start: startTime, start: startTime,
delay: animate ? delay : 0, delay: animate ? delay : 0,
}); });
@@ -290,8 +284,8 @@ export default class BlockScene {
display: { display: {
position: tx.screenPosition position: tx.screenPosition
}, },
duration: animate ? this.animationDuration : 0, duration: animate ? 1000 : 0,
minDuration: animate ? (this.animationDuration / 2) : 0, minDuration: animate ? 500 : 0,
start: startTime, start: startTime,
delay: animate ? delay : 0, delay: animate ? delay : 0,
adjust: animate adjust: animate
@@ -328,11 +322,11 @@ export default class BlockScene {
this.applyTxUpdate(tx, { this.applyTxUpdate(tx, {
display: { display: {
position: { position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)), x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)), y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
} }
}, },
duration: this.animationDuration, duration: 1000,
start: startTime, start: startTime,
delay: 50 delay: 50
}); });

View File

@@ -262,7 +262,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="td-width" i18n="transaction.version">Version</td> <td class="td-width" i18n="transaction.version">Version</td>
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" i18n="tx-features.tag.taproot|Taproot">Taproot</span></td> <td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" >Taproot</span></td>
</tr> </tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.bits">Bits</td> <td i18n="block.bits">Bits</td>

View File

@@ -55,9 +55,7 @@ export class BlocksList implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
if (!this.widget) { this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
this.seoService.setTitle($localize`:@@m8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
}
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) { if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`); this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
} else { } else {

View File

@@ -1,6 +1,6 @@
<div class="container-xl"> <div class="container-xl">
<div class="text-center"> <div class="text-center">
<h2 i18n="shared.calculator">Calculator</h2> <h2>Calculator</h2>
</div> </div>
<ng-container *ngIf="price$ | async; else loading"> <ng-container *ngIf="price$ | async; else loading">
@@ -26,7 +26,7 @@
<div class="input-group input-group-lg mb-1"> <div class="input-group input-group-lg mb-1">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text" i18n="shared.sats">sats</span> <span class="input-group-text">sats</span>
</div> </div>
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)"> <input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard> <app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
@@ -41,7 +41,7 @@
<div class="bitcoin-satoshis-text"> <div class="bitcoin-satoshis-text">
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span> <span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
<span class="sats" i18n="shared.sats">sats</span> <span class="sats"> sats</span>
</div> </div>
</div> </div>

View File

@@ -38,35 +38,36 @@
</div> </div>
<ng-container *ngIf="!hideStats"> <ng-container *ngIf="!hideStats">
<div class="stats top left"> <div class="stats top left">
<p class="label" i18n>Price</p> <p class="label" i18n="clock.fiat-price">fiat price</p>
<p> <p>
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat> <app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
</p> </p>
</div> </div>
<div class="stats top right"> <div class="stats top right">
<p class="label" i18n="fees-box.high-priority">High Priority</p> <p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;"> <p *ngIf="recommendedFees$ | async as recommendedFees;">
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate> <app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate>
</p> </p>
</div> </div>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left"> <div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p> <p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
<p class="label" i18n="block.size">Size</p> <p class="label" i18n="clock.block-size">block size</p>
</div> </div>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right"> <div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right">
<p class="force-wrap"> <p class="force-wrap">
{{ blocks[blockIndex].tx_count | number }} <ng-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].tx_count | number}"></ng-container>
<span class="label" i18n="dashboard.txs">Transactions</span> <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
</p> </p>
</div> </div>
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;"> <ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
<div *ngIf="mode === 'mempool'" class="stats bottom left"> <div *ngIf="mode === 'mempool'" class="stats bottom left">
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p> <p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
<p class="label" i18n="dashboard.memory-usage|Memory usage">Memory Usage</p> <p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
</div> </div>
<div *ngIf="mode === 'mempool'" class="stats bottom right"> <div *ngIf="mode === 'mempool'" class="stats bottom right">
<p>{{ mempoolInfo.size | number }}</p> <p>{{ mempoolInfo.size | number }}</p>
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</p> <p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -63,7 +63,6 @@
.label { .label {
font-size: calc(0.04 * var(--clock-width)); font-size: calc(0.04 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width)); line-height: calc(0.05 * var(--clock-width));
text-transform: lowercase;
} }
&.top { &.top {

View File

@@ -1,24 +0,0 @@
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -1,69 +0,0 @@
.blocks {
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@@ -1,253 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, startWith } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
interface BlockInfo extends BlockExtended {
timeString: string;
}
@Component({
selector: 'app-eight-blocks',
templateUrl: './eight-blocks.component.html',
styleUrls: ['./eight-blocks.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: BlockExtended[] = [];
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
numBlocks: number = 8;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 1080;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
blockInfo: BlockInfo[] = [];
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
maxWidth: '1080px',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = this.padding * 2;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
};
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
}
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
ngAfterViewInit(): void {
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
}
}
await Promise.allSettled(readyPromises);
this.updateBlockGraphs(blocks);
// free up old transactions
previousBlocks.forEach(block => {
if (!newHeights[block.height]) {
delete this.strippedTransactions[block.height];
}
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
}
this.showInfo = false;
setTimeout(() => {
this.blockInfo = blocks.map(block => {
return {
...block,
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
};
});
this.showInfo = true;
}, 1600); // Should match the animation time.
}
setupBlockGraphs(): void {
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@@ -11,7 +11,7 @@
<div class="progress inc-tx-progress-bar"> <div class="progress inc-tx-progress-bar">
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}">&nbsp;</div> <div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}">&nbsp;</div>
<div class="progress-text" *only-vsize>&lrm;{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div> <div class="progress-text" *only-vsize>&lrm;{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
<div class="progress-text" *only-weight>&lrm;{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-per-second|WU/s">WU/s</ng-container></div> <div class="progress-text" *only-weight>&lrm;{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-units-per-second|vB/s">WU/s</ng-container></div>
</div> </div>
</ng-template> </ng-template>
</ng-template> </ng-template>

View File

@@ -22,7 +22,7 @@
<a class="dropdown-item" routerLinkActive="active" <a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a> [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active" <a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.blocks-health">Block Health</a> [routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
</div> </div>
</div> </div>

View File

@@ -7,8 +7,6 @@ import { formatNumber } from '@angular/common';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
@Component({ @Component({
selector: 'app-incoming-transactions-graph', selector: 'app-incoming-transactions-graph',
templateUrl: './incoming-transactions-graph.component.html', templateUrl: './incoming-transactions-graph.component.html',
@@ -31,7 +29,6 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
@Input() left: number | string = '0'; @Input() left: number | string = '0';
@Input() template: ('widget' | 'advanced') = 'widget'; @Input() template: ('widget' | 'advanced') = 'widget';
@Input() windowPreferenceOverride: string; @Input() windowPreferenceOverride: string;
@Input() outlierCappingEnabled: boolean = false;
isLoading = true; isLoading = true;
mempoolStatsChartOption: EChartsOption = {}; mempoolStatsChartOption: EChartsOption = {};
@@ -43,7 +40,6 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
MA: number[][] = []; MA: number[][] = [];
weightMode: boolean = false; weightMode: boolean = false;
rateUnitSub: Subscription; rateUnitSub: Subscription;
medianVbytesPerSecond: number | undefined;
constructor( constructor(
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
@@ -69,35 +65,16 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference'); this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8)); const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize); this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) {
this.computeMedianVbytesPerSecond(this.data.series[0]);
}
this.mountChart(); this.mountChart();
} }
rendered() { rendered() {
if (!this.data) { if (!this.data) {
return; return;
} }
this.isLoading = false; this.isLoading = false;
} }
/**
* Calculate the median value of the vbytes per second chart to hide outliers
*/
computeMedianVbytesPerSecond(data: number[][]): void {
const vBytes: number[] = [];
for (const value of data) {
vBytes.push(value[1]);
}
const sorted = vBytes.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
this.medianVbytesPerSecond = sorted[middle];
if (sorted.length % 2 === 0) {
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
}
}
/// calculate the moving average of the provided data based on windowSize /// calculate the moving average of the provided data based on windowSize
calculateMA(data: number[][], windowSize: number = 100): number[][] { calculateMA(data: number[][], windowSize: number = 100): number[][] {
//update const variables that are not changed //update const variables that are not changed
@@ -255,13 +232,6 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
} }
], ],
yAxis: { yAxis: {
max: (value) => {
if (!this.outlierCappingEnabled || value.max < this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER) {
return undefined;
} else {
return Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
}
},
type: 'value', type: 'value',
axisLabel: { axisLabel: {
fontSize: 11, fontSize: 11,

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="{ val: network$ | async } as network"> <ng-container *ngIf="{ val: network$ | async } as network">
<header class="sticky-header"> <header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;"> <a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -1,11 +1,3 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active { li.nav-item.active {
background-color: #653b9c; background-color: #653b9c;
} }

View File

@@ -5,7 +5,7 @@
<!-- Hamburger --> <!-- Hamburger -->
<ng-container *ngIf="servicesEnabled"> <ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)"> <div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '?md5=' + user.imageMd5" class="profile_image"> <img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images> <app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div> </div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)"> <div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
@@ -18,7 +18,7 @@
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain"> <ng-template [ngIf]="subdomain">
<div class="subdomain_container"> <div class="subdomain_container">
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo"> <img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
</div> </div>
</ng-template> </ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@@ -71,14 +71,13 @@
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a> <a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
</li> </li>
</ul> </ul>
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form> <app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
</div> </div>
</nav> </nav>
</header> </header>
<div class="d-flex" style="overflow: clip"> <div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu> <app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column"> <div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert> <app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>

View File

@@ -238,15 +238,4 @@ nav {
main { main {
transition: 0.2s; transition: 0.2s;
transition-property: max-width; transition-property: max-width;
} }
// empty sidenav
.sidenav {
z-index: 1;
background-color: transparent;
width: 0px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
padding-bottom: 20px;
}

View File

@@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100; positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
return positions; return positions;
}, },
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'}; extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none;`, box-shadow: none;`,
@@ -254,7 +254,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue); const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const { totalValue, totalValueArray } = this.getTotalValues(params); const { totalValue, totalValueArray } = this.getTotalValues(params);
const itemFormatted = []; const itemFormatted = [];
let sum = 0; let totalParcial = 0;
let progressPercentageText = ''; let progressPercentageText = '';
let countItem; let countItem;
let items = this.inverted ? [...params].reverse() : params; let items = this.inverted ? [...params].reverse() : params;
@@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
countItem = items.pop(); countItem = items.pop();
} }
items.map((item: any, index: number) => { items.map((item: any, index: number) => {
sum += item.value[1]; totalParcial += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100; const progressPercentage = (item.value[1] / totalValue) * 100;
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100; const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = ''; let activeItemClass = '';
@@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
<span class="symbol">%</span> <span class="symbol">%</span>
</span> </span>
<span class="total-parcial-vbytes"> <span class="total-parcial-vbytes">
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)} ${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
</span> </span>
<div class="total-percentage-bar"> <div class="total-percentage-bar">
<span class="total-percentage-bar-background"> <span class="total-percentage-bar-background">
@@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</td> </td>
<td class="total-progress-sum"> <td class="total-progress-sum">
<span> <span>
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span> ${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
</span> </span>
</td> </td>
<td class="total-progress-sum"> <td class="total-progress-sum">
<span> <span>
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span> ${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
</span> </span>
</td> </td>
<td class="total-progress-sum-bar"> <td class="total-progress-sum-bar">

View File

@@ -3,11 +3,11 @@
<nav class="scrollable menu-click"> <nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click"> <span *ngIf="userAuth" class="menu-click">
<strong class="menu-click text-nowrap ellipsis">@ {{ userAuth.user.username }}</strong> <strong class="menu-click">@ {{ userAuth.user.username }}</strong>
</span> </span>
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')"> <a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon> <fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
<span class="menu-click" style="font-size: 20px;" i18n="shared.sign-in">Sign in</span> <span class="menu-click" style="font-size: 20px;">Sign in</span>
</a> </a>
<ng-container *ngIf="userMenuGroups$ | async as menuGroups"> <ng-container *ngIf="userMenuGroups$ | async as menuGroups">

View File

@@ -9,27 +9,17 @@
margin-left: -250px; margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000; box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px; padding-bottom: 20px;
@media (max-width: 613px) {
top: 105px;
}
}
.ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
} }
.scrollable { .scrollable {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: scroll;
} }
.sidenav.open { .sidenav.open {
margin-left: 0px; margin-left: 0px;
left: 0px; left: 0px;
display: block; display: block;
background-color: #1d1f31;
} }
.sidenav a, button{ .sidenav a, button{

View File

@@ -44,12 +44,12 @@
</div> </div>
</div> </div>
<!-- Recent blocks --> <!-- Latest blocks -->
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5> <h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a> </a>

View File

@@ -6,7 +6,7 @@
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward"> <div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
<div class="item"> <div class="item">
<h5 class="card-title d-inline-block" i18n="mining.miners-luck" i18n-ngbTooltip="mining.miners-luck-1w" <h5 class="card-title d-inline-block" i18n="mining.miners-luck" i18n-ngbTooltip="mining.miners-luck-1w"
ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools Luck</h5> ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools luck</h5>
<p class="card-text" i18n-ngbTooltip="mining.pools-luck-desc" <p class="card-text" i18n-ngbTooltip="mining.pools-luck-desc"
ngbTooltip="The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes." placement="bottom"> ngbTooltip="The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes." placement="bottom">
{{ miningStats['minersLuck'] }}% {{ miningStats['minersLuck'] }}%
@@ -14,14 +14,14 @@
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title d-inline-block" i18n="mining.miners-count" i18n-ngbTooltip="mining.miners-count-1w" <h5 class="card-title d-inline-block" i18n="mining.miners-count" i18n-ngbTooltip="mining.miners-count-1w"
ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools Count</h5> ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools count</h5>
<p class="card-text" i18n-ngbTooltip="mining.pools-count-desc" <p class="card-text" i18n-ngbTooltip="mining.pools-count-desc"
ngbTooltip="How many unique pools found at least one block over the past week." placement="bottom"> ngbTooltip="How many unique pools found at least one block over the past week." placement="bottom">
{{ miningStats.pools.length }} {{ miningStats.pools.length }}
</p> </p>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title d-inline-block" i18n="shared.blocks-1w" i18n-ngbTooltip="master-page.blocks" <h5 class="card-title d-inline-block" i18n="master-page.blocks" i18n-ngbTooltip="master-page.blocks"
ngbTooltip="Blocks (1w)" placement="bottom" #blockscount [disableTooltip]="!isEllipsisActive(blockscount)">Blocks (1w)</h5> ngbTooltip="Blocks (1w)" placement="bottom" #blockscount [disableTooltip]="!isEllipsisActive(blockscount)">Blocks (1w)</h5>
<p class="card-text" i18n-ngbTooltip="mining.blocks-count-desc" <p class="card-text" i18n-ngbTooltip="mining.blocks-count-desc"
ngbTooltip="The number of blocks found over the past week." placement="bottom"> ngbTooltip="The number of blocks found over the past week." placement="bottom">
@@ -95,7 +95,7 @@
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health" <th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th> i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th> <th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th> <th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
</tr> </tr>
</thead> </thead>
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats"> <tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
@@ -153,19 +153,19 @@
<ng-template #loadingReward> <ng-template #loadingReward>
<div class="pool-distribution"> <div class="pool-distribution">
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck</h5> <h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<p class="card-text"> <p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span> <span class="skeleton-loader skeleton-loader-big"></span>
</p> </p>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.miners-count" >Pools Count</h5> <h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
<p class="card-text"> <p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span> <span class="skeleton-loader skeleton-loader-big"></span>
</p> </p>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="shared.blocks-1w">Blocks (1w)</h5> <h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<p class="card-text"> <p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span> <span class="skeleton-loader skeleton-loader-big"></span>
</p> </p>

View File

@@ -143,7 +143,7 @@
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
@@ -165,7 +165,7 @@
<table class="table table-xs table-data"> <table class="table table-xs table-data">
<thead> <thead>
<tr> <tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
@@ -433,7 +433,7 @@
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>
@@ -458,7 +458,7 @@
<table class="table table-xs table-data text-center"> <table class="table table-xs table-data text-center">
<thead> <thead>
<tr> <tr>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th> <th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr> </tr>

View File

@@ -5,7 +5,7 @@
<br><br> <br><br>
<h2>Privacy Policy</h2> <h2>Privacy Policy</h2>
<h6>Updated: November 23, 2023</h6> <h6>Updated: November 18, 2021</h6>
<br><br> <br><br>
@@ -53,26 +53,6 @@
<br> <br>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<ol>
<li>If you provide your name, country, and/or e-mail address, we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
</ol>
<br>
<p>EOF</p> <p>EOF</p>
</div> </div>

View File

@@ -6,9 +6,11 @@
<form class="formRadioGroup"> <form class="formRadioGroup">
<div class="btn-group btn-group-toggle" name="radioBasic"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf"> <label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
<input type="radio" [value]="'All'" fragment="" [routerLink]="[]"><span i18n="all">All</span></label> <input type="radio" [value]="'All'" fragment="" [routerLink]="[]"> All
</label>
<label class="btn btn-primary btn-sm" [class.active]="fullRbf"> <label class="btn btn-primary btn-sm" [class.active]="fullRbf">
<input type="radio" [value]="'Full RBF'" fragment="fullrbf" [routerLink]="[]" i18n="transaction.full-rbf">Full RBF</label> <input type="radio" [value]="'Full RBF'" fragment="fullrbf" [routerLink]="[]"> Full RBF
</label>
</div> </div>
</form> </form>
</div> </div>
@@ -31,7 +33,7 @@
</div> </div>
<div class="no-replacements" *ngIf="!trees?.length"> <div class="no-replacements" *ngIf="!trees?.length">
<p i18n="rbf.no-replacements-yet">There are no replacements in the mempool yet!</p> <p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
</div> </div>
</ng-container> </ng-container>
</div> </div>

View File

@@ -55,7 +55,7 @@ export class RbfList implements OnInit, OnDestroy {
}) })
); );
this.seoService.setTitle($localize`:@@5e3d5a82750902f159122fcca487b07f1af3141f:RBF Replacements`); this.seoService.setTitle($localize`:@@meta.title.rbf-list:RBF Replacements`);
this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`); this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`);
} }

View File

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

View File

@@ -1,4 +1,4 @@
<form [class]="{hamburgerOpen: hamburgerOpen}" [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate> <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex"> <div class="d-flex">
<div class="search-box-container mr-2"> <div class="search-box-container mr-2">
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem"> <input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">

View File

@@ -26,13 +26,6 @@ form {
@media (min-width: 992px) { @media (min-width: 992px) {
width: 100%; width: 100%;
} }
&.hamburgerOpen {
@media (max-width: 613px) {
margin-left: 0px;
margin-right: 0px;
}
}
} }
.btn-block { .btn-block {

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router'; import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '../../services/assets.service';
@@ -17,8 +17,6 @@ import { SearchResultsComponent } from './search-results/search-results.componen
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SearchFormComponent implements OnInit { export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
network = ''; network = '';
assets: object = {}; assets: object = {};
isSearching = false; isSearching = false;

View File

@@ -14,7 +14,7 @@
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()"> [class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
<div class="small-buttons"> <div class="small-buttons">
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock"> <a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon> <fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" title="Clock view"></fa-icon>
</a> </a>
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv"> <a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon> <fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
@@ -109,26 +109,18 @@
<div> <div>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<div class="vbytes-title"> <div class="d-flex d-md-block align-items-baseline">
<div> <span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span> <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')"> <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon> </button>
</button> </div>
</div>
<div class="form-check">
<input style="margin-top: 9px" class="form-check-input" type="checkbox" [checked]="outlierCappingEnabled" id="hide-outliers" (change)="onOutlierToggleChange($event)">
<label class="form-check-label" for="hide-outliers">
<small i18n="statistics.cap-outliers">Cap outliers</small>
</label>
</div>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="incoming-transactions-graph"> <div class="incoming-transactions-graph">
<app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'" <app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData" [outlierCappingEnabled]="outlierCappingEnabled"></app-incoming-transactions-graph> [data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -222,13 +222,4 @@
border-top-right-radius: 0; border-top-right-radius: 0;
} }
} }
}
.vbytes-title {
display: flex;
align-items: baseline;
justify-content: space-between;
@media (max-width: 767px) {
display: block;
}
} }

View File

@@ -35,7 +35,7 @@ export class StatisticsComponent implements OnInit {
showCount = false; showCount = false;
maxFeeIndex: number; maxFeeIndex: number;
dropDownOpen = false; dropDownOpen = false;
outlierCappingEnabled = false;
mempoolStats: OptimizedMempoolStats[] = []; mempoolStats: OptimizedMempoolStats[] = [];
mempoolVsizeFeesData: any; mempoolVsizeFeesData: any;
@@ -67,7 +67,6 @@ export class StatisticsComponent implements OnInit {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`);
this.stateService.networkChanged$.subscribe((network) => this.network = network); this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h'; this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
this.outlierCappingEnabled = this.storageService.getValue('cap-outliers') === 'true';
this.radioGroupForm = this.formBuilder.group({ this.radioGroupForm = this.formBuilder.group({
dateSpan: this.graphWindowPreference dateSpan: this.graphWindowPreference
@@ -157,6 +156,8 @@ export class StatisticsComponent implements OnInit {
} }
this.maxFeeIndex = maxTier; this.maxFeeIndex = maxTier;
this.capExtremeVbytesValues();
this.mempoolTransactionsWeightPerSecondData = { this.mempoolTransactionsWeightPerSecondData = {
labels: labels, labels: labels,
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])], series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
@@ -210,10 +211,36 @@ export class StatisticsComponent implements OnInit {
} }
}); });
} }
onOutlierToggleChange(e): void { /**
this.outlierCappingEnabled = e.target.checked; * All value higher that "median * capRatio" are capped
this.storageService.setValue('cap-outliers', e.target.checked); */
capExtremeVbytesValues() {
if (this.stateService.network.length !== 0) {
return; // Only cap on Bitcoin mainnet
}
let capRatio = 10;
if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
capRatio = 4;
}
// Find median value
const vBytes: number[] = [];
for (const stat of this.mempoolStats) {
vBytes.push(stat.vbytes_per_second);
}
const sorted = vBytes.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
let median = sorted[middle];
if (sorted.length % 2 === 0) {
median = (sorted[middle - 1] + sorted[middle]) / 2;
}
// Cap
for (const stat of this.mempoolStats) {
stat.vbytes_per_second = Math.min(median * capRatio, stat.vbytes_per_second);
}
} }
onSaveChart(name) { onSaveChart(name) {

View File

@@ -40,7 +40,7 @@
<ng-container [ngSwitch]="extraData"> <ng-container [ngSwitch]="extraData">
<div class="opreturns" *ngSwitchCase="'coinbase'"> <div class="opreturns" *ngSwitchCase="'coinbase'">
<div class="opreturn-row"> <div class="opreturn-row">
<span class="label" i18n="transactions-list.coinbase">Coinbase</span> <span class="label">Coinbase</span>
<span class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</span> <span class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</span>
</div> </div>
</div> </div>

View File

@@ -6,15 +6,12 @@
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate> <app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
</div> </div>
<!-- <div *ngIf="tx && acceleratorAvailable && accelerateCtaType === 'alert' && !tx.status.confirmed && !tx.acceleration" class="alert alert-dismissible alert-purple" role="alert"> <div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
<div> <span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator &trade;</span>
<a class="btn btn-sm blink-bg" (click)="onAccelerateClicked()">Accelerate</a>
<span class="align-middle">this transaction using Mempool Accelerator &trade;</span>
</div>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()"> <button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> --> </div>
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx"> <ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1> <h1 i18n="shared.transaction">Transaction</h1>
@@ -83,12 +80,13 @@
<!-- Accelerator --> <!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary"> <ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3"> <div class="title mt-3" id="acceleratePreviewAnchor">
<h2>Accelerate</h2> <h2>Accelerate</h2>
</div> </div>
<div class="box"> <div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview> <app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div> </div>
</ng-container> </ng-container>
<ng-template #unconfirmedTemplate> <ng-template #unconfirmedTemplate>
@@ -120,7 +118,7 @@
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit"> <ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> <span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span> <span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> <a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span> </span>
</ng-template> </ng-template>
<ng-template #belowBlockLimit> <ng-template #belowBlockLimit>
@@ -130,14 +128,14 @@
<ng-template #timeEstimateDefault> <ng-template #timeEstimateDefault>
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''"> <span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> <a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span> </span>
</ng-template> </ng-template>
</ng-template> </ng-template>
</ng-template> </ng-template>
</td> </td>
</tr> </tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td> <td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
<td> <td>
<app-tx-features [tx]="tx"></app-tx-features> <app-tx-features [tx]="tx"></app-tx-features>

View File

@@ -61,7 +61,7 @@
} }
.btn-small-height { .btn-small-height {
line-height: 1; line-height: 1.1;
} }
.arrow-green { .arrow-green {
@@ -218,33 +218,8 @@
} }
} }
.alert-purple { .link.accelerator {
background-color: #5c3a88; cursor: pointer;
width: 100%;
}
// Blinking block
@keyframes shadowyBackground {
0% {
box-shadow: 0px 0px 20px rgba(#eba814, 1);
}
50% {
box-shadow: 0px 0px 20px rgba(#eba814, .3);
}
100% {
box-shadow: 0px 0px 20px rgba(#ffae00, 1);
}
}
.blink-bg {
color: #fff;
background: repeating-linear-gradient(#daad0a 0%, #daad0a 5%, #987805 100%) !important;
animation: shadowyBackground 1s infinite;
box-shadow: 0px 0px 20px rgba(#eba814, 1);
transition: 100ms all ease-in;
margin-right: 8px;
font-size: 16px;
border: 1px solid gold;
} }
.eta { .eta {
@@ -259,6 +234,7 @@
.accelerate { .accelerate {
display: flex !important; display: flex !important;
align-self: auto; align-self: auto;
margin-top: 3px;
margin-left: auto; margin-left: auto;
background-color: #653b9c; background-color: #653b9c;
@media (max-width: 849px) { @media (max-width: 849px) {

View File

@@ -92,7 +92,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfEnabled: boolean; rbfEnabled: boolean;
taprootEnabled: boolean; taprootEnabled: boolean;
hasEffectiveFeeRate: boolean; hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'button'; accelerateCtaType: 'alert' | 'button' = 'alert';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false; showAccelerationSummary = false;
scrollIntoAccelPreview = false; scrollIntoAccelPreview = false;
@@ -126,7 +126,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
); );
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'button'; this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
this.setFlowEnabled(); this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => { this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
@@ -633,14 +633,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
// simulate normal anchor fragment behavior // simulate normal anchor fragment behavior
applyFragment(): void { applyFragment(): void {
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === ''); const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
if (anchor?.length) { if (anchor) {
if (anchor[0] === 'accelerate') { const anchorElement = document.getElementById(anchor[0]);
setTimeout(this.onAccelerateClicked.bind(this), 100); if (anchorElement) {
} else { anchorElement.scrollIntoView();
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
} }
} }
} }

View File

@@ -156,45 +156,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
if (this.address) { if (this.address) {
const isP2PKUncompressed = this.address.length === 130; const addressIn = tx.vout
const isP2PKCompressed = this.address.length === 66; .filter((v: Vout) => v.scriptpubkey_address === this.address)
if (isP2PKCompressed) { .map((v: Vout) => v.value || 0)
const addressIn = tx.vout .reduce((a: number, b: number) => a + b, 0);
.filter((v: Vout) => v.scriptpubkey === '21' + this.address + 'ac')
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '21' + this.address + 'ac') .filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
.map((v: Vin) => v.prevout.value || 0) .map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0); .reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut; tx['addressValue'] = addressIn - addressOut;
} else if (isP2PKUncompressed) {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey === '41' + this.address + 'ac')
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '41' + this.address + 'ac')
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut;
} else {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey_address === this.address)
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut;
}
} }
this.priceService.getBlockPrice$(tx.status.block_time).pipe( this.priceService.getBlockPrice$(tx.status.block_time).pipe(

View File

@@ -16,7 +16,7 @@
<ng-template #coinbase> <ng-template #coinbase>
<ng-container *ngIf="line.coinbase; else pegin"> <ng-container *ngIf="line.coinbase; else pegin">
<p i18n="transactions-list.coinbase">Coinbase</p> <p>Coinbase</p>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@@ -76,7 +76,7 @@
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks"> <div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5> <h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a> </a>
@@ -99,7 +99,7 @@
<td class="table-cell-badges"> <td class="table-cell-badges">
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> <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-info" i18n="transaction.full-rbf">Full RBF</span>
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span> <span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -110,7 +110,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5> <h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a> </a>
@@ -150,7 +150,7 @@
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5> <h5 class="card-title" i18n="dashboard.latest-transactions">Latest transactions</h5>
<table class="table latest-transactions"> <table class="table latest-transactions">
<thead> <thead>
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th> <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
@@ -233,11 +233,11 @@
</p> </p>
</div> </div>
<div class="item bar"> <div class="item bar">
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5> <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="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
<div class="progress"> <div class="progress">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div> <div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div> <div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
</div> </div>
</div> </div>
</div> </div>
@@ -256,7 +256,7 @@
</ng-template> </ng-template>
<ng-template #txPerSecond let-mempoolInfoData> <ng-template #txPerSecond let-mempoolInfoData>
<h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming Transactions</h5> <h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming transactions</h5>
<ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value" [ngIfElse]="loadingTransactions"> <ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value" [ngIfElse]="loadingTransactions">
<span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync"> <span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
&nbsp;<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span> &nbsp;<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>

View File

@@ -10,9 +10,9 @@
<div class="doc-content"> <div class="doc-content">
<div id="disclaimer"> <div id="disclaimer">
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td> <ng-container *ngTemplateOutlet="faqDisclaimer"></ng-container></td></tr></table> <table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><ng-container *ngTemplateOutlet="faqDisclaimer"></ng-container></div> <div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
<ng-template #faqDisclaimer i18n="faq.big-disclaimer"><p><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></ng-template>
</div> </div>
<div class="doc-item-container" *ngFor="let item of faq"> <div class="doc-item-container" *ngFor="let item of faq">

View File

@@ -64,9 +64,10 @@ export class DocsComponent implements OnInit {
} }
} else { } else {
this.activeTab = 3; this.activeTab = 3;
this.seoService.setTitle($localize`:@@meta.title.docs.electrum:Electrum RPC`); this.seoService.setTitle($localize`:@@meta.title.docs.websocket:Electrum RPC`);
this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`); this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`);
} }
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -37,7 +37,7 @@
<thead> <thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th> <th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="nodedetails text-left">&nbsp;</th> <th class="nodedetails text-left">&nbsp;</th>
<th class="status text-left" i18n="transaction.status|Transaction Status">Status</th> <th class="status text-left" i18n="status">Status</th>
<th class="feerate text-left" *ngIf="status !== 'closed'" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th> <th class="feerate text-left" *ngIf="status !== 'closed'" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="feerate text-left" *ngIf="status === 'closed'" i18n="channels.closing_date">Closing date</th> <th class="feerate text-left" *ngIf="status === 'closed'" i18n="channels.closing_date">Closing date</th>
<th class="liquidity text-right" i18n="lightning.capacity">Capacity</th> <th class="liquidity text-right" i18n="lightning.capacity">Capacity</th>

View File

@@ -14,7 +14,7 @@
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>
<td i18n>Description</td> <td>Description</td>
<td><div class="description-text">These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us! <td><div class="description-text">These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us!
</div> </div>
</td> </td>
@@ -70,7 +70,7 @@
<table class="table table-borderless"> <table class="table table-borderless">
<thead> <thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th> <th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="text-left" i18n="lightning.connect-to-node|Connect">Connect</th> <th class="text-left">Connect</th>
<th class="city text-right d-none d-md-table-cell" i18n="lightning.location">Location</th> <th class="city text-right d-none d-md-table-cell" i18n="lightning.location">Location</th>
</thead> </thead>
<tbody *ngIf="nodes$ | async as response; else skeleton"> <tbody *ngIf="nodes$ | async as response; else skeleton">

View File

@@ -49,7 +49,7 @@
<tr *ngIf="!node.city && !node.country"> <tr *ngIf="!node.city && !node.country">
<td i18n="lightning.location">Location</td> <td i18n="lightning.location">Location</td>
<td> <td>
<span i18n="unknown">Unknown</span> <span>unknown</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -119,7 +119,7 @@
</div> </div>
<ng-template #featurebits let-bits="bits"> <ng-template #featurebits let-bits="bits">
<td i18n="transaction.features|Transaction features" class="text-truncate label">Features</td> <td i18n="lightning.features" class="text-truncate label">Features</td>
<td class="d-flex justify-content-between"> <td class="d-flex justify-content-between">
<span class="text-truncate w-90">{{ bits }}</span> <span class="text-truncate w-90">{{ bits }}</span>
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button> <button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
@@ -133,11 +133,11 @@
<h5>Raw bits</h5> <h5>Raw bits</h5>
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span> <span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
</div> </div>
<h5 i18n="lightning.decoded|Decoded">Decoded</h5> <h5>Decoded</h5>
<table class="table table-borderless table-striped table-fixed"> <table class="table table-borderless table-striped table-fixed">
<thead> <thead>
<th style="width: 13%">Bit</th> <th style="width: 13%">Bit</th>
<th i18n="lightning.as-name">Name</th> <th>Name</th>
<th style="width: 25%; text-align: right">Required</th> <th style="width: 25%; text-align: right">Required</th>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,28 +1,22 @@
<div class="map-wrapper" [class]="style" *ngIf="style !== 'graph'"> <div class="map-wrapper" [class]="style">
<ng-container *ngIf="channelsObservable | async"> <ng-container *ngIf="channelsObservable | async">
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')"> <div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div *ngIf="style === 'graph'" class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
</div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" <div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)"> (chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div> </div>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div> <div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div> </div>
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner"> <div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<div class="full-container-graph" *ngIf="style === 'graph'">
<div class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
</div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div *ngIf="channelsObservable | async" class="chart-graph" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
</div>

View File

@@ -143,55 +143,3 @@
text-align: center; text-align: center;
margin-top: 100px; margin-top: 100px;
} }
.full-container-graph {
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);
}
}
.full-container-graph.widget {
min-height: 240px;
height: 240px;
padding: 0px;
}
.full-container-graph.fit-container {
margin: 0;
padding: 0;
height: 100%;
min-height: 100px;
.chart {
padding: 0;
min-height: 100px;
}
}
.chart-graph {
display: flex;
flex: 1;
height: 100%;
padding-top: 30px;
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;
}
}

View File

@@ -65,7 +65,6 @@ export class NodesChannelsMap implements OnInit {
} }
if (this.style === 'graph') { if (this.style === 'graph') {
this.center = [0, 5];
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`); this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`); this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
} }
@@ -239,6 +238,7 @@ export class NodesChannelsMap implements OnInit {
title: title ?? undefined, title: title ?? undefined,
tooltip: {}, tooltip: {},
geo: { geo: {
top: 75,
animation: false, animation: false,
silent: true, silent: true,
center: this.center, center: this.center,

View File

@@ -7,7 +7,6 @@ import { SharedModule } from './shared/shared.module';
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { AddressComponent } from './components/address/address.component'; import { AddressComponent } from './components/address/address.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { CalculatorComponent } from './components/calculator/calculator.component';
import { BlocksList } from './components/blocks-list/blocks-list.component'; import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component'; import { RbfList } from './components/rbf-list/rbf-list.component';
@@ -88,10 +87,6 @@ const routes: Routes = [
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
}, },
{
path: 'tools/calculator',
component: CalculatorComponent
},
], ],
} }
]; ];
@@ -114,9 +109,12 @@ export class MasterPageRoutingModule { }
], ],
declarations: [ declarations: [
MasterPageComponent, MasterPageComponent,
],
exports: [
MasterPageComponent,
] ]
}) })
export class MasterPageModule { } export class MasterPageModule { }

View File

@@ -311,7 +311,7 @@ export class ApiService {
} }
getEnterpriseInfo$(name: string): Observable<any> { getEnterpriseInfo$(name: string): Observable<any> {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/services/enterprise/info/` + name); return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
} }
getChannelByTxIds$(txIds: string[]): Observable<any[]> { getChannelByTxIds$(txIds: string[]): Observable<any[]> {

View File

@@ -10,7 +10,7 @@ import { StateService } from './state.service';
export class SeoService { export class SeoService {
network = ''; network = '';
baseTitle = 'mempool'; baseTitle = 'mempool';
baseDescription = 'Explore the full Bitcoin ecosystem with The Mempool Open Source Project™.'; baseDescription = 'Explore the full Bitcoin ecosystem with The Mempool Open Project™.';
canonicalLink: HTMLElement = document.getElementById('canonical'); canonicalLink: HTMLElement = document.getElementById('canonical');

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="rateUnits$ | async as units"> <ng-container *ngIf="rateUnits$ | async as units">
<ng-container *ngIf="units !== 'wu'">{{ fee / (weight / 4) | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></ng-container> <ng-container *ngIf="units !== 'wu'">{{ fee / (weight / 4) | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle">sat/vB</span></ng-container>
<ng-container *ngIf="units === 'wu'">{{ fee / weight | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-weight-units|sat/WU">sat/WU</span></ng-container> <ng-container *ngIf="units === 'wu'">{{ fee / weight | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle">sat/WU</span></ng-container>
</ng-container> </ng-container>

View File

@@ -23,12 +23,12 @@
</div> </div>
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login' | relativeUrl]"> <a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login' | relativeUrl]">
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span> <span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
</a> </a>
</div> </div>
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]"> <a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]">
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span> <span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
</a> </a>
<p class="d-none d-sm-block"> <p class="d-none d-sm-block">
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container> <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
@@ -38,45 +38,45 @@
</div> </div>
<div class="row col-md-12 link-tree" [class]="{'services': isServicesPage}"> <div class="row col-md-12 link-tree" [class]="{'services': isServicesPage}">
<div class="links"> <div class="links">
<p class="category" i18n="footer.explore">Explore</p> <p class="category">Explore</p>
<p><a [routerLink]="['/mining' | relativeUrl]" i18n="mining.mining-dashboard">Mining Dashboard</a></p> <p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p> <p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p> <p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p> <p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p> <p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
<p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p> <p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
</div> </div>
<div class="links"> <div class="links">
<p class="category" i18n="footer.learn">Learn</p> <p class="category">Learn</p>
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool" i18n="faq.what-is-a-mempool">What is a mempool?</a></p> <p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool">What is a mempool?</a></p>
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer" i18n="faq.what-is-a-block-exlorer">What is a block explorer?</a></p> <p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer">What is a block explorer?</a></p>
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer" i18n="faq.what-is-a-mempool-exlorer">What is a mempool explorer?</a></p> <p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer">What is a mempool explorer?</a></p>
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool" i18n="faq.why-isnt-my-transaction-confirming">Why isn't my transaction confirming?</a></p> <p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
<p><a [routerLink]="['/docs/faq' | relativeUrl]" i18n="faq.more-faq">More FAQs &raquo;</a></p> <p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs </a></p>
</div> </div>
<div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" > <div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
<p class="category" i18n="footer.networks">Networks</p> <p class="category">Networks</p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')" i18n="footer.mainnet-explorer">Mainnet Explorer</a></p> <p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')" i18n="footer.testnet-explorer">Testnet Explorer</a></p> <p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')" i18n="footer.signet-explorer">Signet Explorer</a></p> <p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')">Signet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')" i18n="footer.liquid-testnet-explorer">Liquid Testnet Explorer</a></p> <p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')">Liquid Testnet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')" i18n="footer.liquid-explorer">Liquid Explorer</a></p> <p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
<p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')" i18n="footer.bisq-explorer">Bisq Explorer</a></p> <p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
</div> </div>
<ng-template #toolBox> <ng-template #toolBox>
<div class="links"> <div class="links">
<p class="category" i18n="footer.tools">Tools</p> <p class="category">Tools</p>
<p><a [routerLink]="['/clock/mempool/0']" i18n="footer.clock-mempool">Clock (Mempool)</a></p> <p><a [routerLink]="['/clock/mempool/0']">Clock (Mempool)</a></p>
<p><a [routerLink]="['/clock/mined/0']" i18n="footer.clock-mined">Clock (Mined)</a></p> <p><a [routerLink]="['/clock/mined/0']">Clock (Mined)</a></p>
<p><a [routerLink]="['/tools/calculator']" i18n="shared.calculator">Calculator</a></p> <p><a [routerLink]="['/tools/calculator']">BTC/Fiat Converter</a></p>
</div> </div>
</ng-template> </ng-template>
<div class="links"> <div class="links">
<p class="category" i18n="footer.legal">Legal</p> <p class="category">Legal</p>
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p> <p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p> <p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
<p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p> <p><a [routerLink]="['/trademark-policy']">Trademark Policy</a></p>
</div> </div>
</div> </div>
<div class="row social-links"> <div class="row social-links">

View File

@@ -1,2 +1,2 @@
<div class="alert" [class]="alertClass" [innerHTML]="errorContent"> <div class="alert alert-danger" [innerHTML]="errorContent">
</div> </div>

View File

@@ -13,7 +13,7 @@ const MempoolErrors = {
'invalid_tx_dependencies': `This transaction dependencies are not valid.`, 'invalid_tx_dependencies': `This transaction dependencies are not valid.`,
'mempool_rejected_raw_tx': `Our mempool rejected this transaction`, 'mempool_rejected_raw_tx': `Our mempool rejected this transaction`,
'no_mining_pool_available': `No mining pool available at the moment`, 'no_mining_pool_available': `No mining pool available at the moment`,
'not_available': `You current subscription does not allow you to access this feature.`, 'not_available': `You current subscription does not allow you to access this feature. Consider <strong><a style="color: #105fb0;" href="/sponsor" target="_blank">upgrading.</a><strong>`,
'not_enough_balance': `Your account balance is too low. Please make a <a style="color:#105fb0" href="/services/accelerator/overview">deposit.</a>`, 'not_enough_balance': `Your account balance is too low. Please make a <a style="color:#105fb0" href="/services/accelerator/overview">deposit.</a>`,
'not_verified': `You must verify your account to use this feature.`, 'not_verified': `You must verify your account to use this feature.`,
'recommended_fees_not_available': `Recommended fees are not available right now.`, 'recommended_fees_not_available': `Recommended fees are not available right now.`,
@@ -33,7 +33,6 @@ export function isMempoolError(error: string) {
}) })
export class MempoolErrorComponent implements OnInit { export class MempoolErrorComponent implements OnInit {
@Input() error: string; @Input() error: string;
@Input() alertClass = 'alert-danger';
errorContent: SafeHtml; errorContent: SafeHtml;
constructor(private sanitizer: DomSanitizer) { } constructor(private sanitizer: DomSanitizer) { }

View File

@@ -12,10 +12,8 @@
<ng-template #ltrTruncated> <ng-template #ltrTruncated>
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span> <span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template> </ng-template>
<ng-template #rtlTruncated> <ng-template #rtlTruncated>
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span> <span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template> </ng-template>

View File

@@ -3,7 +3,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
position: relative;
.truncate-link { .truncate-link {
display: flex; display: flex;
@@ -28,17 +27,4 @@
&.inline { &.inline {
display: inline-flex; display: inline-flex;
} }
}
.hidden-content {
color: transparent;
position: absolute;
max-width: 300px;
overflow: hidden;
}
}
@media (max-width: 567px) {
.hidden-content {
max-width: 150px !important;
}
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */ /* tslint:disable */
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { isNumberFinite, isPositive, isInteger, toDecimal, toSigFigs } from './utils'; import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils';
export type ByteUnit = 'B' | 'kB' | 'MB' | 'GB' | 'TB'; export type ByteUnit = 'B' | 'kB' | 'MB' | 'GB' | 'TB';
@@ -17,7 +17,7 @@ export class BytesPipe implements PipeTransform {
'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'} 'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'}
}; };
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, plaintext = false, sigfigs?: number): any { transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit): any {
if (!(isNumberFinite(input) && if (!(isNumberFinite(input) &&
isNumberFinite(decimal) && isNumberFinite(decimal) &&
@@ -33,35 +33,27 @@ export class BytesPipe implements PipeTransform {
unit = BytesPipe.formats[unit].prev!; unit = BytesPipe.formats[unit].prev!;
} }
let numberFormat = sigfigs == null ?
(number) => toDecimal(number, decimal).toString() :
(number) => toSigFigs(number, sigfigs);
if (to) { if (to) {
const format = BytesPipe.formats[to]; const format = BytesPipe.formats[to];
const result = numberFormat(BytesPipe.calculateResult(format, bytes)); const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
return BytesPipe.formatResult(result, to, plaintext); return BytesPipe.formatResult(result, to);
} }
for (const key in BytesPipe.formats) { for (const key in BytesPipe.formats) {
const format = BytesPipe.formats[key]; const format = BytesPipe.formats[key];
if (bytes < format.max) { if (bytes < format.max) {
const result = numberFormat(BytesPipe.calculateResult(format, bytes)); const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
return BytesPipe.formatResult(result, key, plaintext); return BytesPipe.formatResult(result, key);
} }
} }
} }
static formatResult(result: string, unit: string, plaintext): string { static formatResult(result: number, unit: string): string {
if (plaintext) { return `${result} <span class="symbol">${unit}</span>`;
return `${result} ${unit}`;
} else {
return `${result} <span class="symbol">${unit}</span>`;
}
} }
static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) { static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) {

View File

@@ -54,10 +54,6 @@ export function toDecimal(value: number, decimal: number): number {
return Math.round(value * Math.pow(10, decimal)) / Math.pow(10, decimal); return Math.round(value * Math.pow(10, decimal)) / Math.pow(10, decimal);
} }
export function toSigFigs(value: number, sigFigs: number): string {
return value >= Math.pow(10, sigFigs - 1) ? Math.round(value).toString() : value.toPrecision(sigFigs);
}
export function upperFirst(value: string): string { export function upperFirst(value: string): string {
return value.slice(0, 1).toUpperCase() + value.slice(1); return value.slice(0, 1).toUpperCase() + value.slice(1);
} }

View File

@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck } from '@fortawesome/free-solid-svg-icons'; faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component'; import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@@ -87,7 +87,6 @@ import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/ac
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
import { BlockViewComponent } from '../components/block-view/block-view.component'; import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component'; import { ClockchainComponent } from '../components/clockchain/clockchain.component';
@@ -127,7 +126,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
ColoredPriceDirective, ColoredPriceDirective,
BlockchainComponent, BlockchainComponent,
BlockViewComponent, BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent, MempoolBlockViewComponent,
MempoolBlocksComponent, MempoolBlocksComponent,
BlockchainBlocksComponent, BlockchainBlocksComponent,
@@ -181,7 +179,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
CalculatorComponent, CalculatorComponent,
BitcoinsatoshisPipe, BitcoinsatoshisPipe,
BlockViewComponent, BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent, MempoolBlockViewComponent,
MempoolBlockOverviewComponent, MempoolBlockOverviewComponent,
ClockchainComponent, ClockchainComponent,
@@ -205,7 +202,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FontAwesomeModule, FontAwesomeModule,
], ],
providers: [ providers: [
BytesPipe,
VbytesPipe, VbytesPipe,
WuBytesPipe, WuBytesPipe,
RelativeUrlPipe, RelativeUrlPipe,
@@ -368,6 +364,5 @@ export class SharedModule {
library.addIcons(faUserCheck); library.addIcons(faUserCheck);
library.addIcons(faCircleCheck); library.addIcons(faCircleCheck);
library.addIcons(faUserCircle); library.addIcons(faUserCircle);
library.addIcons(faCheck);
} }
} }

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