Compare commits
138 Commits
mononaut/p
...
mononaut/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa914e7958 | ||
|
|
04eeb19bb9 | ||
|
|
671540af78 | ||
|
|
1a732f18fc | ||
|
|
729fb3bb9d | ||
|
|
fc56f273d4 | ||
|
|
642be969a3 | ||
|
|
d69cdacd5e | ||
|
|
ec8fc53dcb | ||
|
|
108d1762d6 | ||
|
|
879039ca8c | ||
|
|
047d1463e7 | ||
|
|
8fc60fa086 | ||
|
|
bce68ee37f | ||
|
|
2f25c128c1 | ||
|
|
a76a600d3f | ||
|
|
afdb419beb | ||
|
|
8bd2aa3dd3 | ||
|
|
a3f2c42b8e | ||
|
|
dbcd900056 | ||
|
|
5a6d6fae41 | ||
|
|
6bb666ba5e | ||
|
|
505532f812 | ||
|
|
89eb02dad0 | ||
|
|
94a7b710c5 | ||
|
|
1e01f88c15 | ||
|
|
d471616a24 | ||
|
|
9b8f70a0ae | ||
|
|
ab079e9372 | ||
|
|
b77a16233b | ||
|
|
894075493b | ||
|
|
954512cd8e | ||
|
|
deaf6ad6a5 | ||
|
|
98443b48ba | ||
|
|
a7dc8793c2 | ||
|
|
84b8e5d472 | ||
|
|
e768072799 | ||
|
|
206706180f | ||
|
|
0e3b3a0e00 | ||
|
|
28733c1e97 | ||
|
|
7e74a26de2 | ||
|
|
6d920e0ed3 | ||
|
|
88e5f8a6af | ||
|
|
ae82ac8368 | ||
|
|
15cba58144 | ||
|
|
2c820f1cc0 | ||
|
|
4070492584 | ||
|
|
82a43e25e0 | ||
|
|
5e45d8f3bc | ||
|
|
e5709235f3 | ||
|
|
59c513f2a5 | ||
|
|
76a07315f3 | ||
|
|
5723f167df | ||
|
|
6ac328c979 | ||
|
|
85e52d24c3 | ||
|
|
7717c15666 | ||
|
|
61d7fd490a | ||
|
|
ccf952983b | ||
|
|
602d7ce207 | ||
|
|
64a51803c4 | ||
|
|
a63e68e9e3 | ||
|
|
d4d17fa167 | ||
|
|
4cd8d70de5 | ||
|
|
dc26c6f105 | ||
|
|
365954f5b4 | ||
|
|
ff38073280 | ||
|
|
eeebfde33c | ||
|
|
fa12233667 | ||
|
|
19477c4ee3 | ||
|
|
e4b56bac88 | ||
|
|
d390fa8671 | ||
|
|
f9f9c62608 | ||
|
|
d9966143c1 | ||
|
|
57ab82ae7a | ||
|
|
756f6d8abe | ||
|
|
f0840a51d9 | ||
|
|
eac8f8c2c6 | ||
|
|
20f61fc6a0 | ||
|
|
6454892d48 | ||
|
|
35d7c55c1d | ||
|
|
86fe6a802b | ||
|
|
d4568b631d | ||
|
|
2d30c0b588 | ||
|
|
1aea3fcac5 | ||
|
|
5cfd599018 | ||
|
|
d8f2462ff0 | ||
|
|
85091e1f3a | ||
|
|
3a8d19062f | ||
|
|
6913946079 | ||
|
|
fdd18317f9 | ||
|
|
a7c64c0df3 | ||
|
|
f2fb2f98f1 | ||
|
|
7aad664112 | ||
|
|
fa040ca19f | ||
|
|
00887bc24b | ||
|
|
ab8b557e73 | ||
|
|
5c0a59d2f6 | ||
|
|
29cbdf6cd5 | ||
|
|
08b68ef8ba | ||
|
|
1ae34e069c | ||
|
|
5bad829afc | ||
|
|
562cd5683a | ||
|
|
cbf2395009 | ||
|
|
c393483590 | ||
|
|
cbe1ec4e72 | ||
|
|
c6a92083a8 | ||
|
|
8c4b488251 | ||
|
|
3639dcc92a | ||
|
|
a9a3623539 | ||
|
|
27966ad8ec | ||
|
|
5d1ebc6d31 | ||
|
|
a68c2a2be6 | ||
|
|
2bc3352785 | ||
|
|
50460d4025 | ||
|
|
7d3f82eca0 | ||
|
|
20026f974f | ||
|
|
3d964fcdfa | ||
|
|
9e8b2957d0 | ||
|
|
c04e92a686 | ||
|
|
2dd6dd9233 | ||
|
|
e9b72776ed | ||
|
|
23df1f012c | ||
|
|
14a41b3108 | ||
|
|
81d1a809d2 | ||
|
|
a000438277 | ||
|
|
e3fffd8fb2 | ||
|
|
5dfd1a495e | ||
|
|
a463fc289f | ||
|
|
a8e6d9b4b9 | ||
|
|
9ed7e80c44 | ||
|
|
596d55e413 | ||
|
|
0e1a9d8619 | ||
|
|
d09668aaa6 | ||
|
|
93aa08b60d | ||
|
|
4664e55513 | ||
|
|
6199216c54 | ||
|
|
b988a4c526 | ||
|
|
89be841e64 |
@@ -52,6 +52,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000",
|
||||
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 30000,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
@@ -132,6 +133,11 @@
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
},
|
||||
"REPLICATION": {
|
||||
"ENABLED": false,
|
||||
"AUDIT": false,
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 888,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
@@ -140,6 +141,7 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.ESPLORA).toStrictEqual({
|
||||
REST_API_URL: 'http://127.0.0.1:3000',
|
||||
UNIX_SOCKET_PATH: null,
|
||||
BATCH_QUERY_BASE_SIZE: 1000,
|
||||
RETRY_UNIX_SOCKET_AFTER: 30000,
|
||||
REQUEST_TIMEOUT: 10000,
|
||||
FALLBACK_TIMEOUT: 5000,
|
||||
@@ -144,7 +145,8 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.REDIS).toStrictEqual({
|
||||
ENABLED: false,
|
||||
UNIX_SOCKET_PATH: ''
|
||||
UNIX_SOCKET_PATH: '',
|
||||
BATCH_QUERY_BASE_SIZE: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface AbstractBitcoinApi {
|
||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getAllMempoolTransactions(lastTxid: string);
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
|
||||
$getTransactionHex(txId: string): Promise<string>;
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getBlockHashTip(): Promise<string>;
|
||||
|
||||
@@ -77,7 +77,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
|
||||
}
|
||||
|
||||
@@ -573,7 +573,9 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
@@ -590,11 +592,13 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
let lastTxId: string = '';
|
||||
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||
lastTxId = req.query.after_txid;
|
||||
}
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId);
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
|
||||
@@ -8,8 +8,9 @@ import logger from '../../logger';
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
rtts: number[],
|
||||
rtt: number
|
||||
rtt: number,
|
||||
failures: number,
|
||||
latestHeight?: number,
|
||||
socket?: boolean,
|
||||
outOfSync?: boolean,
|
||||
unreachable?: boolean,
|
||||
@@ -92,6 +93,7 @@ class FailoverRouter {
|
||||
host.rtts.unshift(rtt);
|
||||
host.rtts.slice(0, 5);
|
||||
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
||||
host.latestHeight = height;
|
||||
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
|
||||
host.outOfSync = true;
|
||||
} else {
|
||||
@@ -99,22 +101,23 @@ class FailoverRouter {
|
||||
}
|
||||
host.unreachable = false;
|
||||
} else {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sortHosts();
|
||||
|
||||
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('')}`);
|
||||
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
|
||||
|
||||
// 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.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) {
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
@@ -122,6 +125,11 @@ class FailoverRouter {
|
||||
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
|
||||
private sortHosts(): void {
|
||||
// sort by connection quality
|
||||
@@ -156,7 +164,7 @@ class FailoverRouter {
|
||||
private addFailure(host: FailoverHost): FailoverHost {
|
||||
host.failures++;
|
||||
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();
|
||||
return this.activeHost;
|
||||
} else {
|
||||
@@ -225,8 +233,8 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
|
||||
}
|
||||
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
|
||||
@@ -761,8 +761,13 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
|
||||
if (!fastForwarded) {
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
let lastestPriceId;
|
||||
try {
|
||||
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) {
|
||||
await blocksRepository.$saveBlockPrices([{
|
||||
height: blockExtended.height,
|
||||
@@ -771,9 +776,7 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
|
||||
} 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);
|
||||
setTimeout(() => {
|
||||
indexer.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
indexer.scheduleSingleTask('blocksPrices', 10000);
|
||||
}
|
||||
|
||||
// Save blocks summary for visualization if it's enabled
|
||||
|
||||
@@ -44,9 +44,13 @@ export enum FeatureBits {
|
||||
KeysendOptional = 55,
|
||||
ScriptEnforcedLeaseRequired = 2022,
|
||||
ScriptEnforcedLeaseOptional = 2023,
|
||||
SimpleTaprootChannelsRequiredFinal = 80,
|
||||
SimpleTaprootChannelsOptionalFinal = 81,
|
||||
SimpleTaprootChannelsRequiredStaging = 180,
|
||||
SimpleTaprootChannelsOptionalStaging = 181,
|
||||
MaxBolt11Feature = 5114,
|
||||
};
|
||||
|
||||
|
||||
export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
|
||||
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
|
||||
@@ -85,6 +89,10 @@ export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.ZeroConfOptional, 'zero-conf'],
|
||||
[FeatureBits.ShutdownAnySegwitRequired, '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'],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -126,7 +126,7 @@ class Mempool {
|
||||
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
|
||||
while (!done) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
|
||||
if (result) {
|
||||
for (const tx of result) {
|
||||
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
|
||||
@@ -235,7 +235,7 @@ class Mempool {
|
||||
|
||||
if (!loaded) {
|
||||
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
|
||||
const sliceLength = 10000;
|
||||
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
|
||||
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
|
||||
|
||||
@@ -480,14 +480,15 @@ class RbfCache {
|
||||
};
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const sliceLength = 250;
|
||||
let processedCount = 0;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
|
||||
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
|
||||
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
processedCount += slice.length;
|
||||
try {
|
||||
const txs = await bitcoinApi.$getRawTransactions(slice);
|
||||
logger.debug(`fetched ${slice.length} cached rbf transactions`);
|
||||
processTxs(txs);
|
||||
logger.debug(`processed ${slice.length} cached rbf transactions`);
|
||||
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
|
||||
} catch (err) {
|
||||
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ class RedisCache {
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
|
||||
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ interface IConfig {
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
UNIX_SOCKET_PATH: string | void | null;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
RETRY_UNIX_SOCKET_AFTER: number;
|
||||
REQUEST_TIMEOUT: number;
|
||||
FALLBACK_TIMEOUT: number;
|
||||
@@ -151,6 +152,7 @@ interface IConfig {
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ const defaults: IConfig = {
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
'UNIX_SOCKET_PATH': null,
|
||||
'BATCH_QUERY_BASE_SIZE': 1000,
|
||||
'RETRY_UNIX_SOCKET_AFTER': 30000,
|
||||
'REQUEST_TIMEOUT': 10000,
|
||||
'FALLBACK_TIMEOUT': 5000,
|
||||
@@ -303,6 +306,7 @@ const defaults: IConfig = {
|
||||
'REDIS': {
|
||||
'ENABLED': false,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'BATCH_QUERY_BASE_SIZE': 5000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import config from './config';
|
||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
||||
import { LogLevel } from './logger';
|
||||
import logger from './logger';
|
||||
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
class DB {
|
||||
constructor() {
|
||||
@@ -32,7 +34,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
}
|
||||
|
||||
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
{
|
||||
this.checkDBFlag();
|
||||
let hardTimeout;
|
||||
@@ -54,19 +56,38 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
}).then(result => {
|
||||
resolve(result);
|
||||
}).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);
|
||||
}).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
try {
|
||||
const pool = await this.getPool();
|
||||
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 |
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
|
||||
{
|
||||
const pool = await this.getPool();
|
||||
const connection = await pool.getConnection();
|
||||
@@ -75,7 +96,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
const results: [T, FieldPacket[]][] = [];
|
||||
for (const query of queries) {
|
||||
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
|
||||
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
@@ -83,9 +104,8 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
connection.rollback();
|
||||
connection.release();
|
||||
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
this.$rollbackAtomic(connection);
|
||||
throw e;
|
||||
} finally {
|
||||
connection.release();
|
||||
@@ -105,26 +125,43 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
public getPidLock(): boolean {
|
||||
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)) {
|
||||
const pid = fs.readFileSync(filePath).toString();
|
||||
if (pid !== `${process.pid}`) {
|
||||
const msg = `Already running on PID ${pid} (or pid file '${filePath}' is stale)`;
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
if (pid === process.pid) {
|
||||
logger.warn('PID file already exists for this process');
|
||||
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);
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
return true;
|
||||
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(filePath, `${process.pid}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public releasePidLock(): void {
|
||||
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const pid = fs.readFileSync(filePath).toString();
|
||||
if (pid === `${process.pid}`) {
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
// only release our own pid file
|
||||
if (pid === process.pid) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,15 @@ class Server {
|
||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
// Register cleanup listeners for exit events
|
||||
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => {
|
||||
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(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') {
|
||||
bitcoinApi.startHealthChecks();
|
||||
@@ -200,7 +206,7 @@ class Server {
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, pollRate);
|
||||
}
|
||||
@@ -314,14 +320,18 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
onExit(exitEvent): void {
|
||||
onExit(exitEvent, code = 0): void {
|
||||
logger.debug(`onExit for signal: ${exitEvent}`);
|
||||
if (config.DATABASE.ENABLED) {
|
||||
DB.releasePidLock();
|
||||
}
|
||||
process.exit(0);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
onUnhandledException(type, error): void {
|
||||
console.error(`${type}:`, error);
|
||||
this.onExit(type, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
((): Server => new Server())();
|
||||
|
||||
@@ -15,11 +15,18 @@ export interface CoreIndex {
|
||||
best_block_height: number;
|
||||
}
|
||||
|
||||
type TaskName = 'blocksPrices' | 'coinStatsIndex';
|
||||
|
||||
class Indexer {
|
||||
runIndexer = true;
|
||||
indexerRunning = false;
|
||||
tasksRunning: string[] = [];
|
||||
coreIndexes: CoreIndex[] = [];
|
||||
private runIndexer = true;
|
||||
private indexerRunning = false;
|
||||
private tasksRunning: { [key in TaskName]?: boolean; } = {};
|
||||
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
|
||||
private coreIndexes: CoreIndex[] = [];
|
||||
|
||||
public indexerIsRunning(): boolean {
|
||||
return this.indexerRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which core index is available for indexing
|
||||
@@ -69,33 +76,69 @@ class Indexer {
|
||||
}
|
||||
}
|
||||
|
||||
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
|
||||
if (!Common.indexingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
this.tasksRunning.push(task);
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
setTimeout(() => {
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
this.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
/**
|
||||
* schedules a single task to run in `timeout` ms
|
||||
* 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);
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
this.tasksRunning[task] = true;
|
||||
|
||||
switch (task) {
|
||||
case 'blocksPrices': {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
} catch (e) {
|
||||
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
|
||||
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
this.scheduleSingleTask(task, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'coinStatsIndex': {
|
||||
logger.debug(`Indexing coinStatsIndex now`);
|
||||
await mining.$indexCoinStatsIndex();
|
||||
} break;
|
||||
}
|
||||
|
||||
this.tasksRunning[task] = false;
|
||||
}
|
||||
|
||||
public async $run(): Promise<void> {
|
||||
|
||||
@@ -157,4 +157,6 @@ class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||
|
||||
export default new Logger();
|
||||
|
||||
@@ -14,7 +14,7 @@ class NodesSocketsRepository {
|
||||
await DB.query(`
|
||||
INSERT INTO nodes_sockets(public_key, socket, type)
|
||||
VALUE (?, ?, ?)
|
||||
`, [socket.publicKey, socket.addr, socket.network]);
|
||||
`, [socket.publicKey, socket.addr, socket.network], 'silent');
|
||||
} catch (e: any) {
|
||||
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));
|
||||
|
||||
@@ -79,7 +79,7 @@ class ForensicsService {
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
const sliceLength = 1000;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
|
||||
// process batches of 1000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
|
||||
@@ -290,7 +290,7 @@ class NetworkSyncService {
|
||||
|
||||
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||
|
||||
const sliceLength = 5000;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
|
||||
// process batches of 5000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
|
||||
3
contributors/ncois.txt
Normal file
3
contributors/ncois.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
|
||||
|
||||
Signed: ncois
|
||||
3
contributors/shubhamkmr04.txt
Normal file
3
contributors/shubhamkmr04.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 15, 2023.
|
||||
|
||||
Signed: shubhamkmr04
|
||||
3
contributors/starius.txt
Normal file
3
contributors/starius.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
|
||||
|
||||
Signed starius
|
||||
@@ -53,6 +53,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"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__,
|
||||
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
|
||||
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
|
||||
@@ -146,6 +147,7 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"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__
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
|
||||
# ESPLORA
|
||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
||||
__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_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
|
||||
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
|
||||
@@ -148,6 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__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__}"
|
||||
|
||||
@@ -201,6 +203,7 @@ 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_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_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
|
||||
@@ -288,5 +291,6 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
|
||||
# REDIS
|
||||
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_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@@ -31,10 +31,10 @@
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cypress": "^13.6.0",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.4.3",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"mock-socket": "~9.3.1",
|
||||
"ngx-echarts": "~16.2.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
@@ -59,7 +59,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.5.0",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress-fail-on-console-error": "~5.0.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
@@ -7148,9 +7148,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
|
||||
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
|
||||
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -22096,9 +22096,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
|
||||
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
|
||||
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.5.0",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress-fail-on-console-error": "~5.0.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
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 { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
@@ -124,6 +125,10 @@ let routes: Routes = [
|
||||
path: 'view/mempool-block/:index',
|
||||
component: MempoolBlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/blocks',
|
||||
component: EightBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
|
||||
@@ -32,11 +32,22 @@
|
||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||
</video>
|
||||
|
||||
<ng-container *ngIf="false && officialMempoolSpace">
|
||||
<h3 class="mt-5">Sponsor the project</h3>
|
||||
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
|
||||
<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="/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>
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<div id="become-sponsor-container">
|
||||
<div class="become-sponsor community">
|
||||
<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">Become a Community Sponsor</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="/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>
|
||||
</ng-container>
|
||||
|
||||
@@ -193,7 +204,7 @@
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<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'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -205,7 +216,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<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'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -299,9 +310,9 @@
|
||||
<img class="image" src="/resources/profile/blixt.png" />
|
||||
<span>Blixt</span>
|
||||
</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" />
|
||||
<span>Zeus</span>
|
||||
<span>ZEUS</span>
|
||||
</a>
|
||||
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
|
||||
<img class="image" src="/resources/profile/marina.svg" />
|
||||
|
||||
@@ -246,3 +246,48 @@
|
||||
width: 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;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
#become-sponsor-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
max-height: 90vh;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
@@ -18,6 +17,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -58,13 +58,15 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="row" *ngIf="showSuccess">
|
||||
<div class="col" id="successAlert">
|
||||
<div class="alert alert-success">
|
||||
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>.
|
||||
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,8 +74,8 @@
|
||||
<div class="d-flex mb-0">
|
||||
<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)">
|
||||
<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="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | 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>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -87,18 +87,10 @@
|
||||
<h5>Acceleration summary</h5>
|
||||
<div class="row mb-3">
|
||||
<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">
|
||||
<tbody>
|
||||
<!-- ESTIMATED FEE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Next block market rate
|
||||
@@ -121,32 +113,6 @@
|
||||
</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>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MEMPOOL BASE FEE -->
|
||||
<tr>
|
||||
@@ -166,7 +132,7 @@
|
||||
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
</td>
|
||||
@@ -180,8 +146,8 @@
|
||||
</tr>
|
||||
|
||||
<!-- NEXT BLOCK ESTIMATE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<tr class="group-first">
|
||||
<ng-container>
|
||||
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
||||
<td class="item">
|
||||
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
|
||||
</td>
|
||||
@@ -195,7 +161,7 @@
|
||||
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<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>
|
||||
</td>
|
||||
@@ -203,7 +169,7 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- MAX COST -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
|
||||
@@ -228,7 +194,7 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- USER BALANCE -->
|
||||
<ng-container *ngIf="estimate.userBalance < maxCost">
|
||||
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item">
|
||||
Available balance
|
||||
@@ -244,6 +210,17 @@
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
.feerate.active {
|
||||
background-color: #105fb0 !important;
|
||||
opacity: 1;
|
||||
border: 1px solid white !important;
|
||||
border: 1px solid #007fff !important;
|
||||
}
|
||||
.feerate:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.estimateDisabled {
|
||||
@@ -41,6 +44,24 @@
|
||||
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 {
|
||||
tr {
|
||||
text-wrap: wrap;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription, catchError, of, tap } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
@@ -55,14 +56,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
maxCost = 0;
|
||||
userBid = 0;
|
||||
selectFeeRateIndex = 1;
|
||||
showTable: 'estimated' | 'maximum' = 'maximum';
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
|
||||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -73,7 +74,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.scrollEvent) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
|
||||
if (!this.error) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -187,7 +188,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.estimateSubscription.unsubscribe();
|
||||
},
|
||||
error: (response) => {
|
||||
this.error = response.error;
|
||||
if (response.status === 403 && response.error === 'not_available') {
|
||||
this.error = 'waitlisted';
|
||||
} else {
|
||||
this.error = response.error;
|
||||
}
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() blockLimit: number;
|
||||
@Input() orientation = 'left';
|
||||
@Input() flip = true;
|
||||
@Input() animationDuration: number = 1000;
|
||||
@Input() animationOffset: number | null = null;
|
||||
@Input() disableSpinner = false;
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Input() unavailable: boolean = false;
|
||||
@@ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||
if (this.scene) {
|
||||
this.scene.replace(transactions || [], direction, sort);
|
||||
this.scene.replace(transactions || [], direction, sort, startTime);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
@@ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
} else {
|
||||
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,
|
||||
highlighting: this.auditHighlighting });
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ export default class BlockScene {
|
||||
txs: { [key: string]: TxView };
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 1000;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -23,11 +26,11 @@ export default class BlockScene {
|
||||
animateUntil = 0;
|
||||
dirty: boolean;
|
||||
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
) {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
@@ -36,6 +39,7 @@ export default class BlockScene {
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
@@ -90,8 +94,8 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Animate new block entering scene
|
||||
enter(txs: TransactionStripped[], direction) {
|
||||
this.replace(txs, direction);
|
||||
enter(txs: TransactionStripped[], direction, startTime?: number) {
|
||||
this.replace(txs, direction, false, startTime);
|
||||
}
|
||||
|
||||
// Animate block leaving scene
|
||||
@@ -108,8 +112,7 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Reset layout and replace with new set of transactions
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
|
||||
const startTime = performance.now();
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
|
||||
const nextIds = {};
|
||||
const remove = [];
|
||||
txs.forEach(tx => {
|
||||
@@ -133,7 +136,7 @@ export default class BlockScene {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
}, (startTime - performance.now()) + this.animationDuration + 1000);
|
||||
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
|
||||
@@ -147,7 +150,7 @@ export default class BlockScene {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateAll(startTime, 200, direction);
|
||||
this.updateAll(startTime, 50, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
@@ -214,10 +217,13 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
): void {
|
||||
this.animationDuration = animationDuration || 1000;
|
||||
this.configAnimationOffset = animationOffset;
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
@@ -261,8 +267,8 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
|
||||
s: tx.screenPosition.s
|
||||
},
|
||||
color: txColor,
|
||||
@@ -275,7 +281,7 @@ export default class BlockScene {
|
||||
position: tx.screenPosition,
|
||||
color: txColor
|
||||
},
|
||||
duration: animate ? 1000 : 1,
|
||||
duration: animate ? this.animationDuration : 1,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
@@ -284,8 +290,8 @@ export default class BlockScene {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
},
|
||||
duration: animate ? 1000 : 0,
|
||||
minDuration: animate ? 500 : 0,
|
||||
duration: animate ? this.animationDuration : 0,
|
||||
minDuration: animate ? (this.animationDuration / 2) : 0,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
adjust: animate
|
||||
@@ -322,11 +328,11 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
|
||||
}
|
||||
},
|
||||
duration: 1000,
|
||||
duration: this.animationDuration,
|
||||
start: startTime,
|
||||
delay: 50
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
@@ -0,0 +1,69 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { formatNumber } from '@angular/common';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
|
||||
@Component({
|
||||
selector: 'app-incoming-transactions-graph',
|
||||
templateUrl: './incoming-transactions-graph.component.html',
|
||||
@@ -29,6 +31,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
@Input() left: number | string = '0';
|
||||
@Input() template: ('widget' | 'advanced') = 'widget';
|
||||
@Input() windowPreferenceOverride: string;
|
||||
@Input() outlierCappingEnabled: boolean = false;
|
||||
|
||||
isLoading = true;
|
||||
mempoolStatsChartOption: EChartsOption = {};
|
||||
@@ -40,6 +43,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
MA: number[][] = [];
|
||||
weightMode: boolean = false;
|
||||
rateUnitSub: Subscription;
|
||||
medianVbytesPerSecond: number | undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -65,16 +69,35 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
||||
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
|
||||
this.MA = this.calculateMA(this.data.series[0], windowSize);
|
||||
if (this.outlierCappingEnabled === true) {
|
||||
this.computeMedianVbytesPerSecond(this.data.series[0]);
|
||||
}
|
||||
this.mountChart();
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
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
|
||||
calculateMA(data: number[][], windowSize: number = 100): number[][] {
|
||||
//update const variables that are not changed
|
||||
@@ -232,6 +255,13 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
],
|
||||
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',
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Hamburger -->
|
||||
<ng-container *ngIf="servicesEnabled">
|
||||
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
|
||||
</div>
|
||||
<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)">
|
||||
<ng-template [ngIf]="subdomain">
|
||||
<div class="subdomain_container">
|
||||
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
@@ -71,13 +71,14 @@
|
||||
<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>
|
||||
</ul>
|
||||
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="d-flex" style="overflow: clip">
|
||||
<div class="d-flex">
|
||||
<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">
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
|
||||
@@ -238,4 +238,16 @@ nav {
|
||||
main {
|
||||
transition: 0.2s;
|
||||
transition-property: max-width;
|
||||
}
|
||||
}
|
||||
|
||||
// empty sidenav
|
||||
.sidenav {
|
||||
z-index: 1;
|
||||
background-color: transparent;
|
||||
width: 225px;
|
||||
height: calc(100vh - 65px);
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
margin-left: -225px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
|
||||
return positions;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'};
|
||||
background: transparent;
|
||||
border: 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 { totalValue, totalValueArray } = this.getTotalValues(params);
|
||||
const itemFormatted = [];
|
||||
let totalParcial = 0;
|
||||
let sum = 0;
|
||||
let progressPercentageText = '';
|
||||
let countItem;
|
||||
let items = this.inverted ? [...params].reverse() : params;
|
||||
@@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
countItem = items.pop();
|
||||
}
|
||||
items.map((item: any, index: number) => {
|
||||
totalParcial += item.value[1];
|
||||
sum += item.value[1];
|
||||
const progressPercentage = (item.value[1] / totalValue) * 100;
|
||||
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
|
||||
let activeItemClass = '';
|
||||
@@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span class="total-parcial-vbytes">
|
||||
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
|
||||
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)}
|
||||
</span>
|
||||
<div class="total-percentage-bar">
|
||||
<span class="total-percentage-bar-background">
|
||||
@@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
</td>
|
||||
<td class="total-progress-sum">
|
||||
<span>
|
||||
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
|
||||
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="total-progress-sum">
|
||||
<span>
|
||||
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
|
||||
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="total-progress-sum-bar">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<nav class="scrollable menu-click">
|
||||
<span *ngIf="userAuth" class="menu-click">
|
||||
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
|
||||
<strong class="menu-click text-nowrap ellipsis">@ {{ userAuth.user.username }}</strong>
|
||||
</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')">
|
||||
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
|
||||
|
||||
@@ -5,15 +5,24 @@
|
||||
height: calc(100vh - 65px);
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
transition: 0.25s;
|
||||
transition: width 0.25s, left 0.25s, margin 0.25s;
|
||||
margin-left: -250px;
|
||||
box-shadow: 5px 0px 30px 0px #000;
|
||||
padding-bottom: 20px;
|
||||
@media (max-width: 613px) {
|
||||
top: 105px;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidenav.open {
|
||||
|
||||
@@ -53,6 +53,26 @@
|
||||
|
||||
<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 e-mail address, we may contact you regarding your account, billing purposess, or to update you about our services. We will not share this with any third-party.</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(tm), 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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<form [class]="{hamburgerOpen: hamburgerOpen}" [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<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">
|
||||
|
||||
@@ -26,6 +26,13 @@ form {
|
||||
@media (min-width: 992px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.hamburgerOpen {
|
||||
@media (max-width: 613px) {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
@@ -17,6 +17,8 @@ import { SearchResultsComponent } from './search-results/search-results.componen
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() hamburgerOpen = false;
|
||||
|
||||
network = '';
|
||||
assets: object = {};
|
||||
isSearching = false;
|
||||
|
||||
@@ -109,18 +109,26 @@
|
||||
<div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<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')">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="vbytes-title">
|
||||
<div>
|
||||
<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')">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</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 class="card-body">
|
||||
<div class="incoming-transactions-graph">
|
||||
<app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'"
|
||||
[data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph>
|
||||
[data]="mempoolTransactionsWeightPerSecondData" [outlierCappingEnabled]="outlierCappingEnabled"></app-incoming-transactions-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,4 +222,13 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vbytes-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
@media (max-width: 767px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export class StatisticsComponent implements OnInit {
|
||||
showCount = false;
|
||||
maxFeeIndex: number;
|
||||
dropDownOpen = false;
|
||||
|
||||
outlierCappingEnabled = false;
|
||||
mempoolStats: OptimizedMempoolStats[] = [];
|
||||
|
||||
mempoolVsizeFeesData: any;
|
||||
@@ -67,6 +67,7 @@ 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.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
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({
|
||||
dateSpan: this.graphWindowPreference
|
||||
@@ -156,8 +157,6 @@ export class StatisticsComponent implements OnInit {
|
||||
}
|
||||
this.maxFeeIndex = maxTier;
|
||||
|
||||
this.capExtremeVbytesValues();
|
||||
|
||||
this.mempoolTransactionsWeightPerSecondData = {
|
||||
labels: labels,
|
||||
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
|
||||
@@ -211,36 +210,10 @@ export class StatisticsComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* All value higher that "median * capRatio" are capped
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
onOutlierToggleChange(e): void {
|
||||
this.outlierCappingEnabled = e.target.checked;
|
||||
this.storageService.setValue('cap-outliers', e.target.checked);
|
||||
}
|
||||
|
||||
onSaveChart(name) {
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
||||
</div>
|
||||
|
||||
<div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
|
||||
<span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator ™</span>
|
||||
<!-- <div *ngIf="tx && acceleratorAvailable && accelerateCtaType === 'alert' && !tx.status.confirmed && !tx.acceleration" class="alert alert-dismissible alert-purple" role="alert">
|
||||
<div>
|
||||
<a class="btn btn-sm blink-bg" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<span class="align-middle">this transaction using Mempool Accelerator ™</span>
|
||||
</div>
|
||||
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
@@ -80,13 +83,12 @@
|
||||
|
||||
<!-- Accelerator -->
|
||||
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
|
||||
<div class="title mt-3" id="acceleratePreviewAnchor">
|
||||
<div class="title mt-3">
|
||||
<h2>Accelerate</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #unconfirmedTemplate>
|
||||
@@ -118,7 +120,7 @@
|
||||
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
|
||||
<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>
|
||||
<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>
|
||||
<a *ngIf="!tx.acceleration && 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>
|
||||
</ng-template>
|
||||
<ng-template #belowBlockLimit>
|
||||
@@ -128,14 +130,14 @@
|
||||
<ng-template #timeEstimateDefault>
|
||||
<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>
|
||||
<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>
|
||||
<a *ngIf="!tx.acceleration && 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>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor">
|
||||
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
|
||||
@@ -218,8 +218,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.link.accelerator {
|
||||
cursor: pointer;
|
||||
.alert-purple {
|
||||
background-color: #5c3a88;
|
||||
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 {
|
||||
|
||||
@@ -92,7 +92,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
rbfEnabled: boolean;
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
accelerateCtaType: 'alert' | 'button' = 'alert';
|
||||
accelerateCtaType: 'alert' | 'button' = 'button';
|
||||
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
showAccelerationSummary = 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') ?? 'alert';
|
||||
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'button';
|
||||
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
@@ -633,10 +633,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// simulate normal anchor fragment behavior
|
||||
applyFragment(): void {
|
||||
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
|
||||
if (anchor) {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView();
|
||||
if (anchor?.length) {
|
||||
if (anchor[0] === 'accelerate') {
|
||||
setTimeout(this.onAccelerateClicked.bind(this), 100);
|
||||
} else {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,17 +156,45 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
if (this.address) {
|
||||
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 isP2PKUncompressed = this.address.length === 130;
|
||||
const isP2PKCompressed = this.address.length === 66;
|
||||
if (isP2PKCompressed) {
|
||||
const addressIn = tx.vout
|
||||
.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
|
||||
.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);
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '21' + this.address + 'ac')
|
||||
.map((v: Vin) => v.prevout.value || 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(
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,12 +109,9 @@ export class MasterPageRoutingModule { }
|
||||
],
|
||||
declarations: [
|
||||
MasterPageComponent,
|
||||
],
|
||||
exports: [
|
||||
MasterPageComponent,
|
||||
]
|
||||
})
|
||||
export class MasterPageModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getEnterpriseInfo$(name: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/services/enterprise/info/` + name);
|
||||
}
|
||||
|
||||
getChannelByTxIds$(txIds: string[]): Observable<any[]> {
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
</div>
|
||||
<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.sign-in">Sign In / Sign Up</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
|
||||
</a>
|
||||
</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]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
|
||||
</a>
|
||||
<p class="d-none d-sm-block">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
|
||||
@@ -13,7 +13,7 @@ const MempoolErrors = {
|
||||
'invalid_tx_dependencies': `This transaction dependencies are not valid.`,
|
||||
'mempool_rejected_raw_tx': `Our mempool rejected this transaction`,
|
||||
'no_mining_pool_available': `No mining pool available at the moment`,
|
||||
'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_available': `You current subscription does not allow you to access this feature.`,
|
||||
'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.`,
|
||||
'recommended_fees_not_available': `Recommended fees are not available right now.`,
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
|
||||
<ng-template #ltrTruncated>
|
||||
<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 #rtlTruncated>
|
||||
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
|
||||
<div class="hidden-content">{{ text }}</div>
|
||||
</ng-template>
|
||||
@@ -3,6 +3,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
position: relative;
|
||||
|
||||
.truncate-link {
|
||||
display: flex;
|
||||
@@ -27,4 +28,17 @@
|
||||
&.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden-content {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 567px) {
|
||||
.hidden-content {
|
||||
max-width: 150px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils';
|
||||
import { isNumberFinite, isPositive, isInteger, toDecimal, toSigFigs } from './utils';
|
||||
|
||||
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'}
|
||||
};
|
||||
|
||||
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit): any {
|
||||
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, plaintext = false, sigfigs?: number): any {
|
||||
|
||||
if (!(isNumberFinite(input) &&
|
||||
isNumberFinite(decimal) &&
|
||||
@@ -33,27 +33,35 @@ export class BytesPipe implements PipeTransform {
|
||||
unit = BytesPipe.formats[unit].prev!;
|
||||
}
|
||||
|
||||
let numberFormat = sigfigs == null ?
|
||||
(number) => toDecimal(number, decimal).toString() :
|
||||
(number) => toSigFigs(number, sigfigs);
|
||||
|
||||
if (to) {
|
||||
const format = BytesPipe.formats[to];
|
||||
|
||||
const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
|
||||
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
|
||||
|
||||
return BytesPipe.formatResult(result, to);
|
||||
return BytesPipe.formatResult(result, to, plaintext);
|
||||
}
|
||||
|
||||
for (const key in BytesPipe.formats) {
|
||||
const format = BytesPipe.formats[key];
|
||||
if (bytes < format.max) {
|
||||
|
||||
const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
|
||||
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
|
||||
|
||||
return BytesPipe.formatResult(result, key);
|
||||
return BytesPipe.formatResult(result, key, plaintext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static formatResult(result: number, unit: string): string {
|
||||
return `${result} <span class="symbol">${unit}</span>`;
|
||||
static formatResult(result: string, unit: string, plaintext): string {
|
||||
if (plaintext) {
|
||||
return `${result} ${unit}`;
|
||||
} else {
|
||||
return `${result} <span class="symbol">${unit}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) {
|
||||
|
||||
@@ -54,6 +54,10 @@ export function toDecimal(value: number, decimal: number): number {
|
||||
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 {
|
||||
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
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,
|
||||
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';
|
||||
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';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { MenuComponent } from '../components/menu/menu.component';
|
||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
|
||||
@@ -87,6 +87,7 @@ import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/ac
|
||||
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.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 { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
|
||||
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
|
||||
@@ -126,6 +127,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
ColoredPriceDirective,
|
||||
BlockchainComponent,
|
||||
BlockViewComponent,
|
||||
EightBlocksComponent,
|
||||
MempoolBlockViewComponent,
|
||||
MempoolBlocksComponent,
|
||||
BlockchainBlocksComponent,
|
||||
@@ -179,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
CalculatorComponent,
|
||||
BitcoinsatoshisPipe,
|
||||
BlockViewComponent,
|
||||
EightBlocksComponent,
|
||||
MempoolBlockViewComponent,
|
||||
MempoolBlockOverviewComponent,
|
||||
ClockchainComponent,
|
||||
@@ -202,6 +205,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
FontAwesomeModule,
|
||||
],
|
||||
providers: [
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
RelativeUrlPipe,
|
||||
@@ -364,5 +368,6 @@ export class SharedModule {
|
||||
library.addIcons(faUserCheck);
|
||||
library.addIcons(faCircleCheck);
|
||||
library.addIcons(faUserCircle);
|
||||
library.addIcons(faCheck);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="lightgrey" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 464 B After Width: | Height: | Size: 461 B |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 99 KiB |
@@ -519,7 +519,7 @@ html:lang(ru) .card-title {
|
||||
.fees-wrapper-tooltip-chart-advanced,
|
||||
.tx-wrapper-tooltip-chart-advanced {
|
||||
background: rgba(#1d1f31, 0.98);
|
||||
width: 275px;
|
||||
width: 300px;
|
||||
|
||||
thead {
|
||||
th {
|
||||
|
||||
@@ -394,7 +394,7 @@ FREEBSD_PKG=()
|
||||
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
|
||||
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4)
|
||||
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
|
||||
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb105-server keybase)
|
||||
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase)
|
||||
FREEBSD_PKG+=(geoipupdate)
|
||||
|
||||
FREEBSD_UNFURL_PKG=()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"PASSWORD": "__BITCOIN_RPC_PASS__"
|
||||
},
|
||||
"ESPLORA": {
|
||||
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet",
|
||||
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
|
||||
"FALLBACK": [
|
||||
"http://node201.va1.mempool.space:3001",
|
||||
"http://node202.va1.mempool.space:3001",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"PASSWORD": "__BITCOIN_RPC_PASS__"
|
||||
},
|
||||
"ESPLORA": {
|
||||
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet",
|
||||
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
|
||||
"FALLBACK": [
|
||||
"http://node201.va1.mempool.space:3004",
|
||||
"http://node202.va1.mempool.space:3004",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
# kill "while true" loops
|
||||
killall sh
|
||||
|
||||
# kill actual node backends
|
||||
killall node
|
||||
killall node 2>/dev/null
|
||||
|
||||
# kill "while true" loops
|
||||
killall sh 2>/dev/null
|
||||
|
||||
# kill unfurler chrome instances
|
||||
killall chrome
|
||||
killall chrome 2>/dev/null
|
||||
|
||||
# kill xorg
|
||||
killall xinit
|
||||
killall xinit 2>/dev/null
|
||||
|
||||
# kill dbus
|
||||
killall dbus-daemon 2>/dev/null
|
||||
|
||||
# kill nginx cache warmer scripts
|
||||
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
|
||||
|
||||
@@ -1,96 +1,158 @@
|
||||
location /api/v1/services {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1d;
|
||||
expires 1d;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
}
|
||||
location /api/v1/contributors/images {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1w;
|
||||
expires 1w;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
###########
|
||||
# routing #
|
||||
###########
|
||||
|
||||
location /api/v1/assets {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-short;
|
||||
}
|
||||
location /api/v1/contributors {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1d;
|
||||
expires 1d;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
try_files /dev/null @mempool-api-v1-services-cache-medium;
|
||||
}
|
||||
location /api/v1/donations/images {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1w;
|
||||
expires 1w;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
location /api/v1/contributors/images {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-long;
|
||||
}
|
||||
location /api/v1/donations {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1d;
|
||||
expires 1d;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
try_files /dev/null @mempool-api-v1-services-cache-medium;
|
||||
}
|
||||
location /api/v1/translators/images {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1w;
|
||||
expires 1w;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
location /api/v1/donations/images {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-long;
|
||||
}
|
||||
location /api/v1/translators {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-medium;
|
||||
}
|
||||
location /api/v1/translators/images {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-long;
|
||||
}
|
||||
location /api/v1/services/internal/ {
|
||||
return 403;
|
||||
}
|
||||
location /api/v1/services/ {
|
||||
try_files /dev/null @mempool-api-v1-services-cache-disabled;
|
||||
}
|
||||
|
||||
###########
|
||||
# caching #
|
||||
###########
|
||||
|
||||
location @mempool-api-v1-services-cache-disabled {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
|
||||
# remove these just in case double proxied
|
||||
proxy_hide_header Onion-Location;
|
||||
proxy_hide_header Strict-Transport-Security;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# remove cache headers from services backend
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Pragma;
|
||||
|
||||
# pass these headers to services backend
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# add our own cache headers
|
||||
add_header 'Pragma' 'no-cache';
|
||||
add_header 'Cache-Control' 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @mempool-api-v1-services-cache-short {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 1d;
|
||||
expires 1d;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
proxy_redirect off;
|
||||
|
||||
# remove these just in case double proxied
|
||||
proxy_hide_header Onion-Location;
|
||||
proxy_hide_header Strict-Transport-Security;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# pass these headers to services backend
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# remove cache headers from services backend
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Pragma;
|
||||
|
||||
# add our own cache headers
|
||||
add_header 'Pragma' 'public';
|
||||
add_header 'Cache-Control' 'public';
|
||||
expires 2s;
|
||||
|
||||
# server-side cache validity
|
||||
proxy_cache_valid 200 2s;
|
||||
}
|
||||
location /api/v1/assets {
|
||||
|
||||
location @mempool-api-v1-services-cache-medium {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 10m;
|
||||
expires 10m;
|
||||
proxy_hide_header onion-location;
|
||||
proxy_hide_header strict-transport-security;
|
||||
proxy_hide_header content-security-policy;
|
||||
proxy_hide_header x-frame-options;
|
||||
proxy_redirect off;
|
||||
|
||||
# remove these just in case double proxied
|
||||
proxy_hide_header Onion-Location;
|
||||
proxy_hide_header Strict-Transport-Security;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# pass these headers to services backend
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# remove cache headers from services backend
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Pragma;
|
||||
|
||||
# add our own cache headers
|
||||
add_header 'Pragma' 'public';
|
||||
add_header 'Cache-Control' 'public';
|
||||
expires 7d;
|
||||
|
||||
# server-side cache validity
|
||||
proxy_cache_valid 200 7d;
|
||||
}
|
||||
|
||||
location @mempool-api-v1-services-cache-long {
|
||||
proxy_pass $mempoolSpaceServices;
|
||||
proxy_cache services;
|
||||
proxy_redirect off;
|
||||
|
||||
# remove these just in case double proxied
|
||||
proxy_hide_header Onion-Location;
|
||||
proxy_hide_header Strict-Transport-Security;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# pass these headers to services backend
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# remove cache headers from services backend
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Pragma;
|
||||
|
||||
# add our own cache headers
|
||||
add_header 'Pragma' 'public';
|
||||
add_header 'Cache-Control' 'public';
|
||||
expires 30d;
|
||||
|
||||
# server-side cache validity
|
||||
proxy_cache_valid 200 30d;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
# Block the internal APIs of esplora
|
||||
location /api/internal/ {
|
||||
return 404;
|
||||
return 403;
|
||||
}
|
||||
location /api/v1/internal/ {
|
||||
return 404;
|
||||
return 403;
|
||||
}
|
||||
|
||||
# websocket has special HTTP headers
|
||||
|
||||
Reference in New Issue
Block a user