Compare commits

...

57 Commits

Author SHA1 Message Date
hunicus
247c364ccd Update nostr icon to look less dorky 2023-07-05 18:09:48 -04:00
softsimon
7b01f54fc6 Merge pull request #3919 from joostjager/pool-fees
Add average fee delta to pool ranking
2023-07-05 15:30:15 +02:00
wiz
9f2e94d9cd Merge pull request #3863 from mempool/mononaut/scrollable-mempool
scroll to see all mempool blocks
2023-07-05 09:57:17 +09:00
wiz
824c6f97e4 ops: Use mempool/electrs for liquid instances 2023-07-05 09:33:33 +09:00
wiz
efcb58a4a6 Merge branch 'master' into mononaut/scrollable-mempool 2023-07-05 07:58:50 +09:00
Joost Jager
9e5d10b15f Add average fee delta to pool ranking
Co-authored-by: mononaut <83316221+mononaut@users.noreply.github.com>
2023-07-04 11:25:39 +00:00
softsimon
3d33233e51 Merge pull request #3891 from 0xFlicker/master
sign contributor agreement
2023-07-03 10:11:51 +02:00
softsimon
4cd7561af8 Merge pull request #3906 from mempool/simon/mempool-break-limit
Lowering mempool loop break limit
2023-07-03 10:08:17 +02:00
softsimon
52c813bcc7 Merge branch 'master' into simon/mempool-break-limit 2023-07-03 09:59:21 +02:00
softsimon
c20c7ae11f Merge pull request #3917 from mempool/mononaut/effective-rate-templates
Save effective rates to templates & summaries
2023-07-02 18:40:40 +02:00
softsimon
5f582195ad Merge branch 'master' into mononaut/effective-rate-templates 2023-07-02 18:11:28 +02:00
softsimon
a567a28c5c Update backend/src/api/mempool.ts
Co-authored-by: mononaut <83316221+mononaut@users.noreply.github.com>
2023-07-02 18:10:14 +02:00
softsimon
59713e2132 Update backend/src/api/mempool.ts
Co-authored-by: mononaut <83316221+mononaut@users.noreply.github.com>
2023-07-02 18:10:08 +02:00
softsimon
1e83a99bae Update backend/src/api/mempool.ts
Co-authored-by: mononaut <83316221+mononaut@users.noreply.github.com>
2023-07-02 18:10:02 +02:00
softsimon
b5fdb6d64f Merge pull request #3843 from mempool/mononaut/projected-fee-graph
Better projected fee graph
2023-07-02 17:44:12 +02:00
softsimon
1411b701f9 Merge pull request #3887 from mempool/mononaut/atomic-cpfp
Atomic CPFP database operations
2023-07-02 17:29:42 +02:00
softsimon
f80257c5ea Merge branch 'master' into mononaut/atomic-cpfp 2023-07-01 22:04:14 +02:00
softsimon
23cd063402 Merge pull request #3883 from mempool/hunicus/ronindojo-link
Update ronindojo link on about page
2023-07-01 22:03:43 +02:00
softsimon
2bda12e5f9 Merge pull request #3881 from mempool/mononaut/separate-audit-api
Separate summary and audit-summary API endpoints
2023-07-01 19:43:20 +02:00
softsimon
2e285c8d86 Merge pull request #3878 from mempool/mononaut/full-mempool-graph
Add 'all time' option for mempool graph
2023-07-01 17:23:03 +02:00
softsimon
70854de6ec Merge pull request #3855 from mempool/mononaut/websocket-responses
Fix inconsistent websocket responses
2023-07-01 17:19:22 +02:00
softsimon
f5cf5c7cc3 Merge pull request #3916 from mempool/mononaut/fix-transaction-eta
Fix transaction component ETA
2023-07-01 16:21:18 +02:00
nymkappa
d939391e62 ALL -> all (no caps needed, match the url parameters as well) 2023-06-30 19:52:06 -04:00
nymkappa
85e33e689d break graph toggles into another line earlier 2023-06-30 19:52:05 -04:00
Mononaut
ca1a1228a9 break graph toggles into two rows on small screens 2023-06-30 19:52:05 -04:00
Mononaut
d4ed238ae1 Add statistics/all to cache warmer, change query keyword 2023-06-30 19:52:05 -04:00
Mononaut
22baf4186e Add 'all time' option for mempool graph 2023-06-30 19:52:05 -04:00
Mononaut
58b8052530 don't reset blockchain position on every mempool update 2023-06-30 19:45:53 -04:00
Mononaut
9d606d0006 scroll selected mempool block into view 2023-06-30 19:45:53 -04:00
Mononaut
d848ab4bef scroll to see all mempool blocks 2023-06-30 19:45:53 -04:00
Mononaut
eaad63a082 frontend resync recent blocks when necessary 2023-06-30 19:43:04 -04:00
Mononaut
ca6ddd609d clean up backend websocket logic 2023-06-30 19:43:04 -04:00
Mononaut
eca40f94c9 use power-of-ten formatting for large fee rates 2023-06-30 19:41:12 -04:00
Mononaut
86f51e3902 fix fee graph for underfilled blocks 2023-06-30 19:27:31 -04:00
Mononaut
9f2b98b246 Handle stack-of-N-blocks in new fee graph 2023-06-30 19:27:31 -04:00
Mononaut
e4f3642082 Redesign mempool block fee distribution graph 2023-06-30 19:27:30 -04:00
Mononaut
48d62a1396 Save effective fee rates to block templates & summaries 2023-06-29 19:24:19 -04:00
Mononaut
d9ed02a033 Fix transaction component ETA 2023-06-29 11:39:13 -04:00
softsimon
31ae382f75 Lowering mempool loop break limit 2023-06-27 18:46:28 +02:00
softsimon
5f787db30d Merge pull request #3762 from knorrium/update_node_matrix
Update node CI test matrix
2023-06-26 18:08:53 +02:00
softsimon
da3c39c7d0 Merge pull request #3861 from mempool/hunicus/add-luminex-sponsor
Add luminex as enterprise sponsor
2023-06-26 12:31:03 +02:00
0xflick
dbf759fc76 sign contributor agreement 2023-06-24 16:30:29 -05:00
wiz
e88cf70719 Merge pull request #3886 from mempool/mononaut/hotfix-undefined-cpfp-cluster
Hotfix for undefined cpfp cluster bug
2023-06-23 19:08:27 +09:00
Mononaut
1f442b9ea6 Make cpfp db save operations atomic 2023-06-22 12:08:47 -04:00
Mononaut
9ff5ce0d37 Change order of cpfp db operations 2023-06-21 19:57:18 -04:00
Mononaut
329c635da5 Fix getCpfpInfo error handling 2023-06-21 19:57:09 -04:00
Mononaut
e18f3800be Hotfix for undefined cpfp cluster bug 2023-06-21 19:07:00 -04:00
hunicus
9bfc2c9413 Update ronindojo link on about page 2023-06-20 22:36:29 -04:00
Mononaut
4fbab08586 Separate summary and audit-summary API endpoints 2023-06-20 15:13:52 -04:00
hunicus
816f410855 Make luminex css class unique 2023-06-14 22:17:05 -04:00
hunicus
73d629d319 Add luminex as enterprise sponsor on about page 2023-06-14 18:58:49 -04:00
Felipe Knorr Kuhn
048399574e Merge branch 'master' into update_node_matrix 2023-06-12 22:42:21 -07:00
Felipe Knorr Kuhn
68e9fb0882 Merge branch 'master' into update_node_matrix 2023-05-13 14:27:45 -07:00
softsimon
50a96ce340 Merge branch 'master' into update_node_matrix 2023-05-12 10:37:23 -05:00
Felipe Knorr Kuhn
38162e1af2 Merge branch 'master' into update_node_matrix 2023-05-12 07:44:07 -07:00
Felipe Knorr Kuhn
b0478f6604 Remove node 19 and 20 due to Angular incompatibility 2023-05-11 22:15:29 -07:00
Felipe Knorr Kuhn
6cae3256a9 Update node CI test matrix 2023-05-11 21:42:19 -07:00
46 changed files with 869 additions and 473 deletions

View File

@@ -9,7 +9,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16.16.0", "18.14.1"]
node: ["16", "17", "18"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -55,7 +55,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16.16.0", "18.14.1"]
node: ["16", "17", "18"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"

View File

@@ -224,7 +224,12 @@ class BitcoinRoutes {
} else {
let cpfpInfo;
if (config.DATABASE.ENABLED) {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
res.status(500).send('failed to get CPFP info');
return;
}
}
if (cpfpInfo) {
res.json(cpfpInfo);
@@ -394,9 +399,13 @@ class BitcoinRoutes {
private async getBlockAuditSummary(req: Request, res: Response) {
try {
const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`audit not available`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}

View File

@@ -158,6 +158,13 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.stripTransactions(transactions),
};
}
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
block.tx.forEach(tx => {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
@@ -646,7 +653,7 @@ class Blocks {
}
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
// start async callbacks
@@ -668,12 +675,13 @@ class Blocks {
for (let i = 10; i >= 0; --i) {
const newBlock = await this.$indexBlock(lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block`);
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
this.updateTimerProgress(timer, `reindexed block summary`);
let cpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) {
await this.$indexCPFP(newBlock.id, lastBlock.height - i);
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
}
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
this.updateTimerProgress(timer, `reindexed block summary`);
}
await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
@@ -704,7 +712,7 @@ class Blocks {
// Save blocks summary for visualization if it's enabled
if (Common.blocksSummariesIndexingEnabled() === true) {
await this.$getStrippedBlockTransactions(blockExtended.id, true);
await this.$getStrippedBlockTransactions(blockExtended.id, true, false, cpfpSummary, blockExtended.height);
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
}
if (config.MEMPOOL.CPFP_INDEXING) {
@@ -730,6 +738,11 @@ class Blocks {
this.currentDifficulty = block.difficulty;
}
// wait for pending async callbacks to finish
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
await Promise.all(callbackPromises);
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
this.blocks.push(blockExtended);
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
@@ -746,11 +759,6 @@ class Blocks {
diskCache.$saveCacheToDisk();
}
// wait for pending async callbacks to finish
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
await Promise.all(callbackPromises);
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
handledBlocks++;
}
@@ -827,7 +835,7 @@ class Blocks {
}
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
skipDBLookup = false): Promise<TransactionStripped[]>
skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionStripped[]>
{
if (skipMemoryCache === false) {
// Check the memory cache
@@ -845,13 +853,35 @@ class Blocks {
}
}
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
const summary = this.summarizeBlock(block);
let height = blockHeight;
let summary: BlockSummary;
if (cpfpSummary) {
summary = {
id: hash,
transactions: cpfpSummary.transactions.map(tx => {
return {
txid: tx.txid,
fee: tx.fee,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize
};
}),
};
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
summary = this.summarizeBlock(block);
height = block.height;
}
if (height == null) {
const block = await bitcoinApi.$getBlock(hash);
height = block.height;
}
// Index the response if needed
if (Common.blocksSummariesIndexingEnabled() === true) {
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions);
}
return summary.transactions;
@@ -1007,19 +1037,11 @@ class Blocks {
}
public async $getBlockAuditSummary(hash: string): Promise<any> {
let summary;
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
summary = await BlocksAuditsRepository.$getBlockAudit(hash);
return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
return null;
}
// fallback to non-audited transaction summary
if (!summary?.transactions?.length) {
const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
summary = {
transactions: strippedTransactions
};
}
return summary;
}
public getLastDifficultyAdjustmentTime(): number {
@@ -1050,9 +1072,13 @@ class Blocks {
}
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
if (!result) {
await cpfpRepository.$insertProgressMarker(height);
try {
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
if (!result) {
await cpfpRepository.$insertProgressMarker(height);
}
} catch (e) {
// not a fatal error, we'll try again next time the indexer runs
}
}
}

View File

@@ -113,6 +113,10 @@ export class Common {
};
}
static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] {
return txs.map(this.stripTransaction);
}
static sleep$(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {

View File

@@ -143,7 +143,7 @@ class MempoolBlocks {
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
onlineStats = true;
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
feeStatsCalculator.processNext(tx);
}
}
@@ -334,7 +334,7 @@ class MempoolBlocks {
if (hasBlockStack) {
stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
}
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = [];

View File

@@ -156,7 +156,7 @@ class Mempool {
}
};
let loggerTimer = new Date().getTime() / 1000;
let intervalTimer = Date.now();
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
@@ -179,18 +179,20 @@ class Mempool {
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 4) {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
loggerTimer = new Date().getTime() / 1000;
}
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
if (this.inSync && (new Date().getTime()) - start > 10_000) {
logger.debug('Breaking mempool loop because the 10s time limit exceeded.');
break;
if (Date.now() - intervalTimer > 5_000) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
break;
} else {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
intervalTimer = Date.now()
}
}
}

View File

@@ -106,6 +106,7 @@ class Mining {
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
slug: poolInfo.slug,
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
avgFeeDelta: poolInfo.avgFeeDelta,
};
poolsStats.push(poolStat);
});

View File

@@ -211,7 +211,7 @@ class StatisticsApi {
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
FROM statistics \
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
ORDER BY statistics.added DESC;`;
}
@@ -259,7 +259,7 @@ class StatisticsApi {
vsize_1800,
vsize_2000 \
FROM statistics \
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
ORDER BY statistics.added DESC;`;
}
@@ -386,6 +386,17 @@ class StatisticsApi {
}
}
public async $listAll(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(43200, 'all'); // 12h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {
logger.err('$listAll() error' + (e instanceof Error ? e.message : e));
return [];
}
}
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => {
return {

View File

@@ -15,10 +15,11 @@ class StatisticsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/all', this.$getStatisticsByTime.bind(this, 'all'))
;
}
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y' | 'all', req: Request, res: Response) {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@@ -26,10 +27,6 @@ class StatisticsRoutes {
try {
let result;
switch (time as string) {
case '2h':
result = await statisticsApi.$list2H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
break;
case '24h':
result = await statisticsApi.$list24H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
@@ -58,8 +55,13 @@ class StatisticsRoutes {
case '4y':
result = await statisticsApi.$list4Y();
break;
case 'all':
result = await statisticsApi.$listAll();
break;
default:
result = await statisticsApi.$list2H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
break;
}
res.json(result);
} catch (e) {

View File

@@ -22,6 +22,14 @@ import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
// valid 'want' subscriptions
const wantable = [
'blocks',
'mempool-blocks',
'live-2h-chart',
'stats',
];
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
private extraInitProperties = {};
@@ -30,7 +38,7 @@ class WebsocketHandler {
private numConnected = 0;
private numDisconnected = 0;
private initData: { [key: string]: string } = {};
private socketData: { [key: string]: string } = {};
private serializedInitData: string = '{}';
constructor() { }
@@ -39,28 +47,28 @@ class WebsocketHandler {
this.wss = wss;
}
setExtraInitProperties(property: string, value: any) {
setExtraInitData(property: string, value: any) {
this.extraInitProperties[property] = value;
this.setInitDataFields(this.extraInitProperties);
this.updateSocketDataFields(this.extraInitProperties);
}
private setInitDataFields(data: { [property: string]: any }): void {
private updateSocketDataFields(data: { [property: string]: any }): void {
for (const property of Object.keys(data)) {
if (data[property] != null) {
this.initData[property] = JSON.stringify(data[property]);
this.socketData[property] = JSON.stringify(data[property]);
} else {
delete this.initData[property];
delete this.socketData[property];
}
}
this.serializedInitData = '{'
+ Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
+ '}';
+ Object.keys(this.socketData).map(key => `"${key}": ${this.socketData[key]}`).join(', ')
+ '}';
}
private updateInitData(): void {
private updateSocketData(): void {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
const da = difficultyAdjustment.getDifficultyAdjustment();
this.setInitDataFields({
this.updateSocketDataFields({
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks,
@@ -94,11 +102,33 @@ class WebsocketHandler {
const parsedMessage: WebsocketResponse = JSON.parse(message);
const response = {};
if (parsedMessage.action === 'want') {
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
const wantNow = {};
if (parsedMessage && parsedMessage.action === 'want' && Array.isArray(parsedMessage.data)) {
for (const sub of wantable) {
const key = `want-${sub}`;
const wants = parsedMessage.data.includes(sub);
if (wants && client['wants'] && !client[key]) {
wantNow[key] = true;
}
client[key] = wants;
}
client['wants'] = true;
}
// send initial data when a client first starts a subscription
if (wantNow['want-blocks'] || (parsedMessage && parsedMessage['refresh-blocks'])) {
response['blocks'] = this.socketData['blocks'];
}
if (wantNow['want-mempool-blocks']) {
response['mempool-blocks'] = this.socketData['mempool-blocks'];
}
if (wantNow['want-stats']) {
response['mempoolInfo'] = this.socketData['mempoolInfo'];
response['vBytesPerSecond'] = this.socketData['vBytesPerSecond'];
response['fees'] = this.socketData['fees'];
response['da'] = this.socketData['da'];
}
if (parsedMessage && parsedMessage['track-tx']) {
@@ -109,21 +139,21 @@ class WebsocketHandler {
if (parsedMessage['watch-mempool']) {
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
if (rbfCacheTxid) {
response['txReplaced'] = {
response['txReplaced'] = JSON.stringify({
txid: rbfCacheTxid,
};
});
client['track-tx'] = null;
} else {
// It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[trackTxid];
if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx;
response['tx'] = JSON.stringify(tx);
} else {
// tx.prevout is missing from transactions when in bitcoind mode
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
response['tx'] = fullTx;
response['tx'] = JSON.stringify(fullTx);
} catch (e) {
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -131,7 +161,7 @@ class WebsocketHandler {
} else {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
response['tx'] = fullTx;
response['tx'] = JSON.stringify(fullTx);
} catch (e) {
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
client['track-mempool-tx'] = parsedMessage['track-tx'];
@@ -141,10 +171,10 @@ class WebsocketHandler {
}
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
response['txPosition'] = {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: tx.position,
};
});
}
} else {
client['track-tx'] = null;
@@ -177,10 +207,10 @@ class WebsocketHandler {
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = {
response['projected-block-transactions'] = JSON.stringify({
index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
};
});
} else {
client['track-mempool-block'] = null;
}
@@ -189,23 +219,24 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
client['track-rbf'] = parsedMessage['track-rbf'];
response['rbfLatest'] = JSON.stringify(rbfCache.getRbfTrees(parsedMessage['track-rbf'] === 'fullRbf'));
} else {
client['track-rbf'] = false;
}
}
if (parsedMessage.action === 'init') {
if (!this.initData['blocks']?.length || !this.initData['da']) {
this.updateInitData();
if (!this.socketData['blocks']?.length || !this.socketData['da']) {
this.updateSocketData();
}
if (!this.initData['blocks']?.length) {
if (!this.socketData['blocks']?.length) {
return;
}
client.send(this.serializedInitData);
}
if (parsedMessage.action === 'ping') {
response['pong'] = true;
response['pong'] = JSON.stringify(true);
}
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
@@ -221,7 +252,8 @@ class WebsocketHandler {
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse);
}
} catch (e) {
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
@@ -250,7 +282,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.setInitDataFields({ 'loadingIndicators': indicators });
this.updateSocketDataFields({ 'loadingIndicators': indicators });
const response = JSON.stringify({ loadingIndicators: indicators });
this.wss.clients.forEach((client) => {
@@ -266,7 +298,7 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set');
}
this.setInitDataFields({ 'conversions': conversionRates });
this.updateSocketDataFields({ 'conversions': conversionRates });
const response = JSON.stringify({ conversions: conversionRates });
this.wss.clients.forEach((client) => {
@@ -336,11 +368,21 @@ class WebsocketHandler {
memPool.addToSpendMap(newTransactions);
const recommendedFees = feeApi.getRecommendedFee();
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
// update init data
this.updateInitData();
this.updateSocketDataFields({
'mempoolInfo': mempoolInfo,
'vBytesPerSecond': vBytesPerSecond,
'mempool-blocks': mBlocks,
'transactions': latestTransactions,
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': recommendedFees,
});
// cache serialized objects to avoid stringify-ing the same thing for every client
const responseCache = { ...this.initData };
const responseCache = { ...this.socketData };
function getCachedResponse(key: string, data): string {
if (!responseCache[key]) {
responseCache[key] = JSON.stringify(data);
@@ -371,8 +413,6 @@ class WebsocketHandler {
}
}
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -490,7 +530,7 @@ class WebsocketHandler {
if (rbfReplacedBy) {
response['rbfTransaction'] = JSON.stringify({
txid: rbfReplacedBy,
})
});
}
const rbfChange = rbfChanges.map[client['track-tx']];
@@ -524,9 +564,7 @@ class WebsocketHandler {
}
if (Object.keys(response).length) {
const serializedResponse = '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse);
}
});
@@ -562,14 +600,7 @@ class WebsocketHandler {
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
return {
txid: tx.txid,
vsize: tx.vsize,
fee: tx.fee ? Math.round(tx.fee) : 0,
value: tx.value,
};
}) : [];
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
let totalFees = 0;
let totalWeight = 0;
@@ -633,11 +664,19 @@ class WebsocketHandler {
const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee();
const mempoolInfo = memPool.getMempoolInfo();
// update init data
this.updateInitData();
this.updateSocketDataFields({
'mempoolInfo': mempoolInfo,
'blocks': [...blocks.getBlocks(), block].slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT),
'mempool-blocks': mBlocks,
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': fees,
});
const responseCache = { ...this.initData };
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
responseCache[key] = JSON.stringify(data);
@@ -645,22 +684,26 @@ class WebsocketHandler {
return responseCache[key];
}
const mempoolInfo = memPool.getMempoolInfo();
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (!client['want-blocks']) {
return;
const response = {};
if (client['want-blocks']) {
response['block'] = getCachedResponse('block', block);
}
const response = {};
response['block'] = getCachedResponse('block', block);
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
response['fees'] = getCachedResponse('fees', fees);
if (client['want-stats']) {
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
response['fees'] = getCachedResponse('fees', fees);
if (da?.previousTime) {
response['da'] = getCachedResponse('da', da);
}
}
if (mBlocks && client['want-mempool-blocks']) {
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
@@ -755,11 +798,19 @@ class WebsocketHandler {
}
}
const serializedResponse = '{'
if (Object.keys(response).length) {
const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse);
}
});
}
// takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object
private serializeResponse(response): string {
return '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
client.send(serializedResponse);
});
}
private printLogs(): void {

View File

@@ -30,7 +30,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{
this.checkDBFlag();
let hardTimeout;
@@ -45,7 +45,9 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`));
}, hardTimeout);
this.getPool().then(pool => {
// Use a specific connection if provided, otherwise delegate to the pool
const connectionPromise = connection ? Promise.resolve(connection) : this.getPool();
connectionPromise.then((pool: PoolConnection | Pool) => {
return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
}).then(result => {
resolve(result);
@@ -61,6 +63,33 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}
}
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
{
const pool = await this.getPool();
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const results: [T, FieldPacket[]][] = [];
for (const query of queries) {
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
results.push(result);
}
await connection.commit();
return results;
} catch (e) {
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
connection.rollback();
connection.release();
throw e;
} finally {
connection.release();
}
}
public async checkDbConnection() {
this.checkDBFlag();
try {

View File

@@ -150,7 +150,7 @@ class Server {
if (config.BISQ.ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
bisqMarkets.startBisqService();
}

View File

@@ -19,6 +19,7 @@ export interface PoolInfo {
blockCount: number;
slug: string;
avgMatchRate: number | null;
avgFeeDelta: number | null;
}
export interface PoolStats extends PoolInfo {

View File

@@ -64,7 +64,6 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count,
transactions,
template,
missing_txs as missingTxs,
added_txs as addedTxs,
@@ -76,7 +75,6 @@ class BlocksAuditRepositories {
FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}"
`);
@@ -85,12 +83,9 @@ class BlocksAuditRepositories {
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template);
if (rows[0].transactions.length) {
return rows[0];
}
return rows[0];
}
return null;
} catch (e: any) {

View File

@@ -5,52 +5,10 @@ import { Ancestor, CpfpCluster } from '../mempool.interfaces';
import transactionRepository from '../repositories/TransactionRepository';
class CpfpRepository {
public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<boolean> {
if (!txs[0]) {
return false;
}
// skip clusters of transactions with the same fees
const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
const equalFee = txs.length > 1 && txs.reduce((acc, tx) => {
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
}, true);
if (equalFee) {
return false;
}
try {
const packedTxs = Buffer.from(this.pack(txs));
await DB.query(
`
INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate)
VALUE (UNHEX(?), ?, ?, ?)
ON DUPLICATE KEY UPDATE
height = ?,
txs = ?,
fee_rate = ?
`,
[clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize]
);
const maxChunk = 10;
let chunkIndex = 0;
while (chunkIndex < txs.length) {
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => {
return { txid: tx.txid, cluster: clusterRoot };
});
await transactionRepository.$batchSetCluster(chunk);
chunkIndex += maxChunk;
}
return true;
} catch (e: any) {
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> {
try {
const clusterValues: any[] = [];
const txs: any[] = [];
const clusterValues: [string, number, Buffer, number][] = [];
const txs: { txid: string, cluster: string }[] = [];
for (const cluster of clusters) {
if (cluster.txs?.length) {
@@ -76,16 +34,10 @@ class CpfpRepository {
return false;
}
const queries: { query, params }[] = [];
const maxChunk = 100;
let chunkIndex = 0;
// insert transactions in batches of up to 100 rows
while (chunkIndex < txs.length) {
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
await transactionRepository.$batchSetCluster(chunk);
chunkIndex += maxChunk;
}
chunkIndex = 0;
// insert clusters in batches of up to 100 rows
while (chunkIndex < clusterValues.length) {
const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
@@ -97,12 +49,23 @@ class CpfpRepository {
return (' (UNHEX(?), ?, ?, ?)');
}) + ';';
const values = chunk.flat();
await DB.query(
queries.push({
query,
values
);
params: values,
});
chunkIndex += maxChunk;
}
chunkIndex = 0;
// insert transactions in batches of up to 100 rows
while (chunkIndex < txs.length) {
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
queries.push(transactionRepository.buildBatchSetQuery(chunk));
chunkIndex += maxChunk;
}
await DB.$atomicQuery(queries);
return true;
} catch (e: any) {
logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
@@ -120,8 +83,8 @@ class CpfpRepository {
[clusterRoot]
);
const cluster = clusterRows[0];
cluster.effectiveFeePerVsize = cluster.fee_rate;
if (cluster?.txs) {
cluster.effectiveFeePerVsize = cluster.fee_rate;
cluster.txs = this.unpack(cluster.txs);
return cluster;
}

View File

@@ -39,7 +39,8 @@ class PoolsRepository {
pools.name AS name,
pools.link AS link,
slug,
AVG(blocks_audits.match_rate) AS avgMatchRate
AVG(blocks_audits.match_rate) AS avgMatchRate,
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta
FROM blocks
JOIN pools on pools.id = pool_id
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height

View File

@@ -25,9 +25,8 @@ class TransactionRepository {
}
}
public async $batchSetCluster(txs): Promise<void> {
try {
let query = `
public buildBatchSetQuery(txs: { txid: string, cluster: string }[]): { query, params } {
let query = `
INSERT IGNORE INTO compact_transactions
(
txid,
@@ -35,13 +34,22 @@ class TransactionRepository {
)
VALUES
`;
query += txs.map(tx => {
return (' (UNHEX(?), UNHEX(?))');
}) + ';';
const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
query += txs.map(tx => {
return (' (UNHEX(?), UNHEX(?))');
}) + ';';
const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
return {
query,
params: values,
};
}
public async $batchSetCluster(txs): Promise<void> {
try {
const query = this.buildBatchSetQuery(txs);
await DB.query(
query,
values
query.query,
query.params,
);
} catch (e: any) {
logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));

View 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 June 24, 2023.
Signed: 0xflicker

View File

@@ -173,6 +173,21 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://www.luminex.io" target="_blank" title="Luminex">
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
<defs>
<style>
.lum-cls-1 {
fill: #f2ea25;
}
</style>
</defs>
<path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
<path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
<rect class="lum-cls-1" width="60.69" height="372.67"/>
</svg>
<span>Luminex</span>
</a>
</div>
</div>
@@ -205,7 +220,7 @@
<img class="image" src="/resources/profile/mynodebtc.png" />
<span>myNode</span>
</a>
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
<a href="https://code.samourai.io/ronindojo/RoninDojo" target="_blank" title="RoninDojo">
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>

View File

@@ -63,7 +63,7 @@
*ngIf="blockAudit?.matchRate != null; else nullHealth"
>{{ blockAudit?.matchRate }}%</span>
<ng-template #nullHealth>
<ng-container *ngIf="!isLoadingAudit; else loadingHealth">
<ng-container *ngIf="!isLoadingOverview; else loadingHealth">
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-container>
</ng-template>

View File

@@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
@@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
strippedTransactions: TransactionStripped[];
overviewTransitionDirection: string;
isLoadingOverview = true;
isLoadingAudit = true;
error: any;
blockSubsidy: number;
fees: number;
@@ -281,143 +280,111 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false;
});
if (!this.auditSupported) {
this.overviewSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of([]);
}),
switchMap((transactions) => {
if (prevBlock) {
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
} else {
return of({ transactions, direction: 'down' });
}
})
)
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
this.setupBlockGraphs();
},
(error) => {
this.error = error;
this.isLoadingOverview = false;
});
}
if (this.auditSupported) {
this.auditSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => {
this.isLoadingAudit = true;
this.blockAudit = null;
return this.apiService.getBlockAudit$(block.id)
this.overviewSubscription = block$.pipe(
switchMap((block) => {
return forkJoin([
this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.isLoadingAudit = false;
return of([]);
return of(null);
})
);
}
),
filter((response) => response != null),
map((response) => {
const blockAudit = response.body;
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
this.numMissing = 0;
this.numUnexpected = 0;
),
!this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of(null);
})
)
]);
})
)
.subscribe(([transactions, blockAudit]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
this.numMissing = 0;
this.numUnexpected = 0;
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoadingOverview = false;
this.isLoadingAudit = false;
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : (isSigop[tx.txid] ? 'sigop' : 'missing');
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
this.blockAudit = blockAudit;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoadingOverview = false;
this.isLoadingAudit = false;
});
}
}
} else {
this.setAuditAvailable(false);
}
this.isLoadingOverview = false;
this.setupBlockGraphs();
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
@@ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy {
}
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
if (!this.auditSupported) {
if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
this.setAuditAvailable(false);
}
}
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
if (!this.auditSupported) {
return false;
}
switch (this.stateService.network) {
case 'testnet':
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
break;
case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
break;
default:
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
this.setAuditAvailable(false);
return false;
}
}
return true;
}
getMinBlockFee(block: BlockExtended): number {

View File

@@ -1,9 +1,9 @@
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
<div class="position-container" [ngClass]="network ? network : ''">
<div class="position-container" [ngClass]="network ? network : ''" [style.--divider-offset]="dividerOffset + 'px'" [style.--mempool-offset]="mempoolOffset + 'px'">
<span>
<div class="blocks-wrapper">
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>
<app-mempool-blocks [hidden]="pageIndex > 0"></app-mempool-blocks>
<app-mempool-blocks [hidden]="pageIndex > 0" [allBlocks]="scrollableMempool" (widthChange)="onMempoolWidthChange($event)"></app-mempool-blocks>
<app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks>
<ng-container *ngFor="let page of pages; trackBy: trackByPageFn">
<app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage" [loadingTip]="loadingTip" [connected]="connected"></app-blockchain-blocks>

View File

@@ -26,43 +26,14 @@
position: absolute;
left: 0;
top: 75px;
transform: translateX(50vw);
--divider-offset: 50vw;
--mempool-offset: 0px;
transform: translateX(calc(var(--divider-offset) + var(--mempool-offset)));
}
.position-container.liquid, .position-container.liquidtestnet {
transform: translateX(420px);
}
@media (min-width: 768px) {
.blockchain-wrapper.time-ltr {
.position-container.liquid, .position-container.liquidtestnet {
transform: translateX(calc(100vw - 420px));
}
}
}
@media (max-width: 767.98px) {
.blockchain-wrapper {
.position-container {
transform: translateX(95vw);
}
.position-container.liquid, .position-container.liquidtestnet {
transform: translateX(50vw);
}
.position-container.loading {
transform: translateX(50vw);
}
}
.blockchain-wrapper.time-ltr {
.position-container {
transform: translateX(5vw);
}
.position-container.liquid, .position-container.liquidtestnet {
transform: translateX(50vw);
}
.position-container.loading {
transform: translateX(50vw);
}
.blockchain-wrapper.time-ltr {
.position-container {
transform: translateX(calc(100vw - var(--divider-offset) - var(--mempool-offset)));
}
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
@@ -13,43 +13,95 @@ export class BlockchainComponent implements OnInit, OnDestroy {
@Input() pageIndex: number;
@Input() blocksPerPage: number = 8;
@Input() minScrollWidth: number = 0;
@Input() scrollableMempool: boolean = false;
@Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter();
network: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
ltrTransitionEnabled = false;
flipping = false;
connectionStateSubscription: Subscription;
loadingTip: boolean = true;
connected: boolean = true;
dividerOffset: number = 0;
mempoolOffset: number = 0;
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
) {}
ngOnInit() {
ngOnInit(): void {
this.onResize();
this.network = this.stateService.network;
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
this.connected = (state === 2);
})
firstValueFrom(this.stateService.chainTip$).then(tip => {
});
firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false;
});
}
ngOnDestroy() {
ngOnDestroy(): void {
this.timeLtrSubscription.unsubscribe();
this.connectionStateSubscription.unsubscribe();
}
trackByPageFn(index: number, item: { index: number }) {
trackByPageFn(index: number, item: { index: number }): number {
return item.index;
}
toggleTimeDirection() {
this.ltrTransitionEnabled = true;
this.stateService.timeLtr.next(!this.timeLtr);
toggleTimeDirection(): void {
this.ltrTransitionEnabled = false;
const prevOffset = this.mempoolOffset;
this.mempoolOffset = 0;
this.mempoolOffsetChange.emit(0);
setTimeout(() => {
this.ltrTransitionEnabled = true;
this.flipping = true;
this.stateService.timeLtr.next(!this.timeLtr);
setTimeout(() => {
this.ltrTransitionEnabled = false;
this.flipping = false;
this.mempoolOffset = prevOffset;
this.mempoolOffsetChange.emit(this.mempoolOffset);
}, 1000);
}, 0);
this.cd.markForCheck();
}
onMempoolWidthChange(width): void {
if (this.flipping) {
return;
}
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
this.cd.markForCheck();
setTimeout(() => {
this.mempoolOffsetChange.emit(this.mempoolOffset);
}, 0);
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 768) {
if (this.stateService.isLiquid()) {
this.dividerOffset = 420;
} else {
this.dividerOffset = window.innerWidth * 0.5;
}
} else {
if (this.stateService.isLiquid()) {
this.dividerOffset = window.innerWidth * 0.5;
} else {
this.dividerOffset = window.innerWidth * 0.95;
}
}
this.cd.markForCheck();
}
}

View File

@@ -1,5 +1,9 @@
import { OnChanges } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
import { selectPowerOfTen } from '../../bitcoin.utils';
@Component({
selector: 'app-fee-distribution-graph',
@@ -7,47 +11,121 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() feeRange: number[];
@Input() vsize: number;
@Input() transactions: TransactionStripped[];
@Input() height: number | string = 210;
@Input() top: number | string = 20;
@Input() right: number | string = 22;
@Input() left: number | string = 30;
@Input() numSamples: number = 200;
@Input() numLabels: number = 10;
simple: boolean = false;
data: number[][];
labelInterval: number = 50;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesInitOptions = {
renderer: 'svg'
};
constructor() { }
constructor(
private stateService: StateService,
private vbytesPipe: VbytesPipe,
) { }
ngOnInit() {
ngOnInit(): void {
this.mountChart();
}
ngOnChanges() {
ngOnChanges(): void {
this.simple = !!this.feeRange?.length;
this.prepareChart();
this.mountChart();
}
mountChart() {
prepareChart(): void {
if (this.simple) {
this.data = this.feeRange.map((rate, index) => [index * 10, rate]);
this.labelInterval = 1;
return;
}
this.data = [];
if (!this.transactions?.length) {
return;
}
const samples = [];
const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
const sampleInterval = maxBlockVSize / this.numSamples;
let cumVSize = 0;
let sampleIndex = 0;
let nextSample = 0;
let txIndex = 0;
this.labelInterval = this.numSamples / this.numLabels;
while (nextSample <= maxBlockVSize) {
if (txIndex >= txs.length) {
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]);
nextSample += sampleInterval;
sampleIndex++;
continue;
}
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]);
nextSample += sampleInterval;
sampleIndex++;
}
cumVSize += txs[txIndex].vsize;
txIndex++;
}
this.data = samples.reverse();
}
mountChart(): void {
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: '20',
top: '22',
left: '30',
left: '40',
},
xAxis: {
type: 'category',
boundaryGap: false,
name: '% Weight',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: {
verticalAlign: 'top',
padding: [30, 0, 0, 0],
},
axisLabel: {
interval: (index: number): boolean => { return index && (index % this.labelInterval === 0); },
formatter: (value: number): string => { return Number(value).toFixed(0); },
},
axisTick: {
interval: (index:number): boolean => { return (index % this.labelInterval === 0); },
},
},
yAxis: {
type: 'value',
// name: 'Effective Fee Rate s/vb',
// nameLocation: 'middle',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
axisLabel: {
formatter: (value: number): string => {
const selectedPowerOfTen = selectPowerOfTen(value);
const newVal = Math.round(value / selectedPowerOfTen.divider);
return `${newVal}${selectedPowerOfTen.unit}`;
},
}
},
series: [{
@@ -58,14 +136,18 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges {
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
formatter: (label: any) => {
return Math.floor(label.data);
formatter: (label: { data: number[] }): string => {
const value = label.data[1];
const selectedPowerOfTen = selectPowerOfTen(value);
const newVal = Math.round(value / selectedPowerOfTen.divider);
return `${newVal}${selectedPowerOfTen.unit}`;
},
},
showAllSymbol: false,
smooth: true,
lineStyle: {
color: '#D81B60',
width: 4,
width: 1,
},
itemStyle: {
color: '#b71c1c',

View File

@@ -39,11 +39,11 @@
</tr>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
</div>
<div class="col-md chart-container">
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
</div>
</div>
</div>

View File

@@ -17,6 +17,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
network$: Observable<string>;
mempoolBlockIndex: number;
mempoolBlock$: Observable<MempoolBlock>;
mempoolBlockTransactions$: Observable<TransactionStripped[]>;
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
previewTx: TransactionStripped | void;
webGlEnabled: boolean;
@@ -53,6 +54,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
this.ordinal$.next(ordinal);
this.seoService.setTitle(ordinal);
mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize;
return mempoolBlocks[this.mempoolBlockIndex];
})
);
@@ -62,6 +64,8 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
})
);
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
this.network$ = this.stateService.networkChanged$;
}

View File

@@ -1,9 +1,9 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { take, map, switchMap } from 'rxjs/operators';
import { take, map, switchMap, tap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -29,6 +29,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() count: number = null;
@Input() spotlight: number = 0;
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
@Input() allBlocks: boolean = false;
@Output() widthChange: EventEmitter<number> = new EventEmitter();
specialBlocks = specialBlocks;
mempoolBlocks: MempoolBlock[] = [];
@@ -143,16 +146,24 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
this.now = Date.now();
this.updateMempoolBlockStyles();
this.calculateTransactionPosition();
return this.mempoolBlocks;
}),
tap(() => {
this.cd.markForCheck();
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
})
);
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
.pipe(
map((da) => {
this.now = new Date().getTime();
this.now = Date.now();
this.cd.markForCheck();
return da;
})
);
@@ -254,7 +265,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
if (!this.allBlocks) {
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
}
while (blocks.length < blocksAmount) {
blocks.push({
blockSize: 0,
@@ -274,10 +288,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
let blocksAmount;
let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
if (this.count) {
blocksAmount = 8;
} else {
} else if (!this.allBlocks) {
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
}
while (blocks.length > blocksAmount) {

View File

@@ -94,7 +94,8 @@
<th class="" i18n="master-page.blocks">Blocks</th>
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
<th class="d-none d-md-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
</tr>
</thead>
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
@@ -121,7 +122,15 @@
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-template>
</td>
<td class="d-none d-md-table-cell">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
<td *ngIf="auditAvailable" class="d-none d-sm-table-cell">
<span *ngIf="pool.avgFeeDelta != null; else nullFeeDelta" class="difference" [class.positive]="pool.avgFeeDelta >= 0" [class.negative]="pool.avgFeeDelta < 0">
{{ pool.avgFeeDelta > 0 ? '+' : '' }}{{ (pool.avgFeeDelta * 100) | amountShortener: 2 }}%
</span>
<ng-template #nullFeeDelta>
-
</ng-template>
</td>
<td class="d-none d-lg-table-cell">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr>
<tr style="border-top: 1px solid #555">
<td class="d-none d-md-table-cell"></td>

View File

@@ -110,4 +110,15 @@
.disabled {
pointer-events: none;
opacity: 0.5;
}
}
td {
.difference {
&.positive {
color: rgb(66, 183, 71);
}
&.negative {
color: rgb(183, 66, 66);
}
}
}

View File

@@ -18,7 +18,7 @@
(dragstart)="onDragStart($event)"
(scroll)="onScroll($event)"
>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth" [scrollableMempool]="true" (mempoolOffsetChange)="onMempoolOffsetChange($event)"></app-blockchain>
</div>
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>

View File

@@ -1,6 +1,6 @@
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
import { Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { MarkBlockState, StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
@Component({
@@ -24,6 +24,7 @@ export class StartComponent implements OnInit, OnDestroy {
chainTipSubscription: Subscription;
chainTip: number = -1;
tipIsSet: boolean = false;
lastMark: MarkBlockState;
markBlockSubscription: Subscription;
blockCounterSubscription: Subscription;
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
@@ -40,10 +41,11 @@ export class StartComponent implements OnInit, OnDestroy {
minScrollWidth: number;
pageIndex: number = 0;
pages: any[] = [];
pendingMark: number | void = null;
pendingMark: number | null = null;
lastUpdate: number = 0;
lastMouseX: number;
velocity: number = 0;
mempoolOffset: number = 0;
constructor(
private stateService: StateService,
@@ -70,19 +72,40 @@ export class StartComponent implements OnInit, OnDestroy {
this.chainTip = height;
this.tipIsSet = true;
this.updatePages();
if (this.pendingMark != null) {
this.scrollToBlock(this.pendingMark);
this.pendingMark = null;
}
this.applyPendingMarkArrow();
});
this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => {
let blockHeight;
let newMark = true;
if (mark?.blockHeight != null) {
if (this.lastMark?.blockHeight === mark.blockHeight) {
newMark = false;
}
blockHeight = mark.blockHeight;
} else if (mark?.mempoolBlockIndex != null) {
if (this.lastMark?.mempoolBlockIndex === mark.mempoolBlockIndex || (mark.txid && this.lastMark?.txid === mark.txid)) {
newMark = false;
}
blockHeight = -1 - mark.mempoolBlockIndex;
} else if (mark?.mempoolPosition?.block != null) {
if (this.lastMark?.txid === mark.txid) {
newMark = false;
}
blockHeight = -1 - mark.mempoolPosition.block;
}
this.lastMark = mark;
if (blockHeight != null) {
if (this.tipIsSet) {
if (!this.blockInViewport(mark.blockHeight)) {
this.scrollToBlock(mark.blockHeight);
let scrollToHeight = blockHeight;
if (blockHeight < 0) {
scrollToHeight = this.chainTip - blockHeight;
}
} else {
this.pendingMark = mark.blockHeight;
if (newMark && !this.blockInViewport(scrollToHeight)) {
this.scrollToBlock(scrollToHeight);
}
}
if (!this.tipIsSet || (blockHeight < 0 && !this.mempoolOffset)) {
this.pendingMark = blockHeight;
}
}
});
@@ -117,6 +140,24 @@ export class StartComponent implements OnInit, OnDestroy {
});
}
onMempoolOffsetChange(offset): void {
const delta = offset - this.mempoolOffset;
this.addConvertedScrollOffset(delta);
this.mempoolOffset = offset;
this.applyPendingMarkArrow();
}
applyPendingMarkArrow(): void {
if (this.pendingMark != null) {
if (this.pendingMark < 0) {
this.scrollToBlock(this.chainTip - this.pendingMark);
} else {
this.scrollToBlock(this.pendingMark);
}
this.pendingMark = null;
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
@@ -350,7 +391,7 @@ export class StartComponent implements OnInit, OnDestroy {
resetScroll(): void {
this.scrollToBlock(this.chainTip);
this.blockchainContainer.nativeElement.scrollLeft = 0;
this.setScrollLeft(0);
}
getPageIndexOf(height: number): number {
@@ -368,9 +409,17 @@ export class StartComponent implements OnInit, OnDestroy {
getConvertedScrollOffset(): number {
if (this.timeLtr) {
return -this.blockchainContainer?.nativeElement?.scrollLeft || 0;
return -(this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
} else {
return this.blockchainContainer?.nativeElement?.scrollLeft || 0;
return (this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
}
}
setScrollLeft(offset: number): void {
if (this.timeLtr) {
this.blockchainContainer.nativeElement.scrollLeft = offset - this.mempoolOffset;
} else {
this.blockchainContainer.nativeElement.scrollLeft = offset + this.mempoolOffset;
}
}
@@ -388,7 +437,7 @@ export class StartComponent implements OnInit, OnDestroy {
ngOnDestroy() {
if (this.blockchainContainer?.nativeElement) {
// clean up scroll position to prevent caching wrong scroll in Firefox
this.blockchainContainer.nativeElement.scrollLeft = 0;
this.setScrollLeft(0);
}
this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();

View File

@@ -20,39 +20,46 @@
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a>
</div>
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
(LIVE)
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
24H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
</label>
<div class="btn-toggle-rows" name="radioBasic">
<div class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
(LIVE)
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
24H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
</label>
</div>
<div class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input type="radio" [value]="'all'" [routerLink]="['/graphs' | relativeUrl]" fragment="all" formControlName="dateSpan"><span i18n="all">All</span>
</label>
</div>
</div>
<div class="small-buttons">
<div ngbDropdown #myDrop="ngbDropdown">

View File

@@ -53,17 +53,17 @@
}
}
.formRadioGroup.mining {
@media (min-width: 991px) {
@media (min-width: 1035px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
@media (min-width: 830px) and (max-width: 1035px) {
position: relative;
top: 0px;
}
}
.formRadioGroup.no-menu {
@media (min-width: 991px) {
@media (min-width: 1035px) {
position: relative;
top: -33px;
}
@@ -183,3 +183,43 @@
}
}
}
.btn-toggle-rows {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: stretch;
.btn-group {
flex-grow: 1;
flex-shrink: 1;
}
@media (min-width: 500px) {
.btn-group:first-child > .btn:last-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group:last-child > .btn:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
@media (max-width: 499px) {
flex-direction: column;
.btn-group:first-child > .btn:first-child {
border-bottom-left-radius: 0;
}
.btn-group:first-child > .btn:last-child {
border-bottom-right-radius: 0;
}
.btn-group:last-child > .btn:first-child {
border-top-left-radius: 0;
}
.btn-group:last-child > .btn:last-child {
border-top-right-radius: 0;
}
}
}

View File

@@ -72,8 +72,10 @@ export class StatisticsComponent implements OnInit {
this.route
.fragment
.subscribe((fragment) => {
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) {
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} else {
this.radioGroupForm.controls.dateSpan.setValue('2h', { emitEvent: false });
}
});
@@ -114,7 +116,12 @@ export class StatisticsComponent implements OnInit {
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
return this.apiService.list3YStatistics$();
}
return this.apiService.list4YStatistics$();
if (this.radioGroupForm.controls.dateSpan.value === '4y') {
return this.apiService.list4YStatistics$();
}
if (this.radioGroupForm.controls.dateSpan.value === 'all') {
return this.apiService.listAllTimeStatistics$();
}
})
)
.subscribe((mempoolStats: any) => {

View File

@@ -105,7 +105,7 @@
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * this.mempoolPosition.block) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time>
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
</ng-template>
</ng-template>

View File

@@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition } from '../../interfaces/node-api.interface';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
@@ -65,7 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchCachedTx$ = new Subject<string>();
isCached: boolean = false;
now = Date.now();
timeAvg$: Observable<number>;
da$: Observable<DifficultyAdjustment>;
liquidUnblinding = new LiquidUnblinding();
inputIndex: number;
outputIndex: number;
@@ -117,11 +117,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setFlowEnabled();
});
this.timeAvg$ = timer(0, 1000)
.pipe(
switchMap(() => this.stateService.difficultyAdjustment$),
map((da) => da.timeAvg)
);
this.da$ = this.stateService.difficultyAdjustment$.pipe(
tap(() => {
this.now = Date.now();
})
);
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.fragmentParams = new URLSearchParams(fragment || '');
@@ -236,10 +236,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
this.now = Date.now();
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.mempoolPosition = txPosition.position;
if (this.tx && !this.tx.status.confirmed) {
this.stateService.markBlock$.next({
txid: txPosition.txid,
mempoolPosition: this.mempoolPosition
});
this.txInBlockIndex = this.mempoolPosition.block;
@@ -359,6 +361,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txid: tx.txid,
txFeePerVSize: tx.effectiveFeePerVsize,
mempoolPosition: this.mempoolPosition,
});
@@ -434,12 +437,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
this.now = Date.now();
if (!this.tx || this.mempoolPosition) {
return;
}
this.now = Date.now();
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);

View File

@@ -91,6 +91,7 @@ export interface SinglePoolStats {
logo: string;
slug: string;
avgMatchRate: number;
avgFeeDelta: number;
}
export interface PoolsStats {
blockCount: number;
@@ -153,6 +154,8 @@ export interface BlockExtended extends Block {
export interface BlockAudit extends BlockExtended {
missingTxs: string[],
addedTxs: string[],
freshTxs: string[],
sigopTxs: string[],
matchRate: number,
expectedFees: number,
expectedWeight: number,
@@ -169,6 +172,7 @@ export interface TransactionStripped {
vsize: number;
value: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
}
interface RbfTransaction extends TransactionStripped {

View File

@@ -31,6 +31,7 @@ export interface WebsocketResponse {
'track-rbf'?: string;
'watch-mempool'?: boolean;
'track-bisq-market'?: string;
'refresh-blocks'?: boolean;
}
export interface ReplacedTransaction extends Transaction {

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface';
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
@@ -72,6 +72,10 @@ export class ApiService {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y');
}
listAllTimeStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/all');
}
getTransactionTimes$(txIds: string[]): Observable<number[]> {
let params = new HttpParams();
txIds.forEach((txId: string) => {
@@ -245,9 +249,9 @@ export class ApiService {
);
}
getBlockAudit$(hash: string) : Observable<any> {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' }
getBlockAudit$(hash: string) : Observable<BlockAudit> {
return this.httpClient.get<BlockAudit>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
);
}

View File

@@ -1,15 +1,16 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { map, shareReplay } from 'rxjs/operators';
import { map, scan, shareReplay, tap } from 'rxjs/operators';
import { StorageService } from './storage.service';
interface MarkBlockState {
export interface MarkBlockState {
blockHeight?: number;
txid?: string;
mempoolBlockIndex?: number;
txFeePerVSize?: number;
mempoolPosition?: MempoolPosition;
@@ -100,6 +101,7 @@ export class StateService {
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>();
@@ -166,6 +168,30 @@ export class StateService {
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
this.liveMempoolBlockTransactions$ = merge(
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => {
if (change.transactions) {
const txMap = {}
change.transactions.forEach(tx => {
txMap[tx.txid] = tx;
})
return txMap;
} else {
change.delta.changed.forEach(tx => {
transactions[tx.txid].rate = tx.rate;
})
change.delta.removed.forEach(txid => {
delete transactions[txid];
});
change.delta.added.forEach(tx => {
transactions[tx.txid] = tx;
});
return transactions;
}
}, {}));
if (this.env.BASE_MODULE === 'bisq') {
this.network = this.env.BASE_MODULE;
this.networkChanged$.next(this.env.BASE_MODULE);

View File

@@ -235,6 +235,8 @@ export class WebsocketService {
}
handleResponse(response: WebsocketResponse) {
let reinitBlocks = false;
if (response.blocks && response.blocks.length) {
const blocks = response.blocks;
let maxHeight = 0;
@@ -256,9 +258,11 @@ export class WebsocketService {
}
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
if (response.block.height === this.stateService.latestBlockHeight + 1) {
this.stateService.updateChainTip(response.block.height);
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
reinitBlocks = true;
}
if (response.txConfirmed) {
@@ -369,5 +373,9 @@ export class WebsocketService {
if (response['git-commit']) {
this.stateService.backendInfo$.next(response['git-commit']);
}
if (reinitBlocks) {
this.websocketSubject.next({'refresh-blocks': true});
}
}
}

View File

@@ -67,7 +67,15 @@
<div class="col-sm-12">
<a href="https://github.com/mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
<a href="https://twitter.com/mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a>
<a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank"><svg fill="#fff" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 875 875"><path d="M684.72 485.57c.22 12.59-11.93 51.47-38.67 81.3-26.74 29.83-56.02 20.85-58.42 20.16s-3.09-4.46-7.89-3.77-9.6 6.17-18.86 7.2-17.49 1.71-26.06-1.37c-4.46.69-5.14.71-7.2 2.24s-17.83 10.79-21.6 11.47c0 7.2-1.37 44.57 0 55.89s3.77 25.71 7.54 36 2.74 10.63 7.54 9.94 13.37.34 15.77 4.11c2.4 3.77 1.37 6.51 5.49 8.23s60.69 17.14 99.43 19.2c26.74.69 42.86 2.74 52.12 19.54 1.37 7.89 7.54 13.03 11.31 14.06s8.23 2.06 12 5.83 1.03 8.23 5.49 11.66c4.46 3.43 14.74 8.57 25.37 13.71 10.63 5.14 15.09 13.37 15.77 16.11s1.71 10.97 1.71 10.97-8.91 0-10.97-2.06-2.74-5.83-2.74-5.83-6.17 1.03-7.54 3.43.69 2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8 4.11 8.23 5.83 8.23 10.63 10.97s8.23 5.83 8.23 5.83l-7.2 4.46s-4.46 2.06-14.74-.69-11.66-4.46-12.69-10.63 0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77 4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97 0-19.2 3.43-19.89 1.71-26.74-14.06-55.89-19.89-64.12c-13.03 1.03-50.74-.69-50.74-.69s-2.4-.69-17.49 5.83-36.48 13.76-46.77 19.93-14.4 9.7-16.12 13.13c.12 3-1.23 7.72-2.79 9.06s-12.48 2.42-12.48 2.42-5.85 5.86-8.25 9.97c-6.86 9.6-55.2 125.14-66.52 149.83-13.54 32.57-9.77 27.43-37.71 27.43s-8.06.3-8.06.3-12.34 5.88-16.8 5.88-18.86-2.4-26.4 0-16.46 9.26-23.31 10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12s1.74-6.51 7.91-10.88c8.23-5.83 25.37-16.11 34.63-21.26s17.49-7.89 23.31-9.26 18.51-6.17 30.51-9.94 19.54-8.23 29.83-31.54 50.4-111.43 51.43-116.23c.63-2.96 3.73-6.48 4.8-15.09.66-5.35-2.49-13.04 1.71-22.63 10.97-25.03 21.6-20.23 26.4-20.23s17.14.34 26.4-1.37 15.43-2.74 24.69-7.89 11.31-8.91 11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2 1.03 8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17 13.03-1.71-18.86-5.14 7.2-2.74-16.11-4.8 8.23-3.43-14.4-5.83 4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46 3.43-4.46 3.43l1.37 12-12.2-6.27-7-11.9s2.36 4.01-9.62 7.53c-20.55 0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89 3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26 25.71-22.97 25.03-16.46 46.29-17.14c21.26-.69 32.91 2.74 46.29 8.23s38.74 13.71 43.89 17.49c11.31-9.94 28.46-19.89 34.29-19.89 1.03-2.4 6.19-12.33 17.96-17.6 35.31-15.81 108.13-34 131.53-35.54 31.2-2.06 7.89-1.37 39.09 2.06 31.2 3.43 54.17 7.54 69.6 12.69 12.58 4.19 25.03 9.6 34.29 2.06 4.33-1.81 11.81-1.34 17.83-5.14 30.69-25.09 34.72-32.35 43.63-41.95s20.14-24.91 22.54-45.14 4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03 2.06-12 5.14-22.29 6.86-30.51s9.94-14.74 19.89-16.46c9.94-1.71 17.83 1.37 22.29 4.8 4.46 3.43 11.65 6.28 13.37 10.29.34 1.71-1.37 6.51 8.23 8.23 9.6 1.71 16.05 4.16 16.05 4.16s15.64 4.29 3.11 7.73c-12.69 2.06-20.52-.71-24.29 1.69s-7.21 10.08-9.61 11.1-7.2.34-12 4.11-9.6 6.86-12.69 14.4-5.49 15.77-3.43 26.74 8.57 31.54 14.4 43.2c5.83 11.66 20.23 40.8 24.34 47.66s15.77 29.49 16.8 53.83 1.03 44.23 0 54.86-10.84 51.65-35.53 85.94c-8.16 14.14-23.21 31.9-24.67 35.03-1.45 3.13-3.02 4.88-1.61 7.65 4.62 9.05 12.87 22.13 14.71 29.22 2.29 6.64 6.99 16.13 7.22 28.72Z" style="stroke:#000;stroke-miterlimit:10;stroke-width:6px"/></svg></a>
<a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank">
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" height="20" width="auto">
<path d="M10.1763 4.02459C11.0916 4.0629 11.8843 4.65378 12.4402 5.55882C12.8446 5.12801 13.3938 4.83598 14.1209 4.73381C14.1711 4.727 14.2784 4.72444 14.3661 4.72359C12.871 3.08634 10.7263 2.05359 8.3356 2.05359C7.89798 2.05359 7.47143 2.09786 7.05254 2.16427C6.96569 2.16853 6.85586 2.18385 6.71197 2.21621C6.70601 2.21748 6.70005 2.21855 6.6941 2.21961C6.68814 2.22068 6.68217 2.22174 6.67622 2.22302C6.67134 2.22406 6.66747 2.22468 6.66371 2.22528C6.65828 2.22614 6.65311 2.22696 6.64557 2.22898C4.99469 2.63339 3.75334 2.30475 3.04753 1.34692C2.97346 1.2456 2.67036 0.791806 2.5767 0C2.05649 0.537236 1.71678 1.4261 2.00881 2.3635C2.2421 3.11018 2.66865 3.51715 3.13863 3.74703C2.4243 3.79045 1.82832 3.65678 1.23233 3.20639C0.880705 2.93905 0.665298 2.65127 0.299194 1.89352C-0.0558415 2.41033 -0.0243404 3.00376 0.0412177 3.31622C0.128912 3.72234 0.342616 4.18295 0.612512 4.44518C1.03311 4.85726 1.62824 4.94326 2.10672 4.9407C1.40602 5.76486 0.862823 6.7278 0.531627 7.7861C2.08031 7.4366 4.04739 7.30373 5.49059 7.20625C5.80877 7.18475 6.10149 7.16498 6.35864 7.14499C6.80393 5.32639 8.0623 3.93604 10.1763 4.02459Z" fill="#fff"/>
<path d="M14.3867 11.1813C15.2124 11.1813 15.8817 10.0168 15.8817 8.58029C15.8817 7.14377 15.2124 5.97925 14.3867 5.97925C13.561 5.97925 12.8916 7.14377 12.8916 8.58029C12.8916 10.0168 13.561 11.1813 14.3867 11.1813Z" fill="#fff"/>
<path d="M10.177 11.9852C11.2373 11.9852 12.0969 10.5165 12.0969 8.70478C12.0969 6.89303 11.2373 5.42432 10.177 5.42432C9.11665 5.42432 8.25708 6.89303 8.25708 8.70478C8.25708 10.5165 9.11665 11.9852 10.177 11.9852Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.97102 35.7932V28.701C6.97102 28.701 6.81437 24.6849 8.33413 21.6863C8.86711 20.6348 10.0863 19.1908 10.9956 18.4032C11.4843 17.9792 11.9654 17.6183 12.4422 17.2871C12.6848 17.1457 12.9181 16.9925 13.1437 16.8273C17.1224 14.385 20.639 14.8946 23.5546 15.3171L23.6535 15.3314C23.6535 15.3314 24.355 13.2522 21.4764 12.1837C19.9132 11.6031 18.0614 11.1416 16.5221 10.813C16.3016 11.2148 16.0206 11.552 15.6843 11.8117C15.4493 11.993 14.855 12.4494 13.7755 12.3208C13.2331 12.2561 12.8151 12.0339 12.4839 11.7121C11.9254 12.6375 11.1148 13.2471 10.174 13.3263C7.57382 13.5468 6.26606 11.3715 6.18092 8.95776C4.23461 9.14848 1.57567 10.3456 0.158935 11.0446V33.6816C2.25987 34.7341 4.55274 35.4602 6.97102 35.7932Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.158935 33.6816C2.25987 34.7341 4.55274 35.4602 6.97102 35.7932V28.701C6.97102 28.701 6.81437 24.6849 8.33413 21.6863C8.86711 20.6348 10.0863 19.1908 10.9956 18.4032C11.4843 17.9792 11.9654 17.6183 12.4422 17.2871C12.6848 17.1457 12.9181 16.9925 13.1437 16.8273C17.1224 14.385 20.639 14.8946 23.5546 15.3171L23.6535 15.3314C23.6535 15.3314 24.355 13.2522 21.4764 12.1837C19.9132 11.6031 18.0614 11.1416 16.5221 10.813C16.3016 11.2148 16.0206 11.552 15.6843 11.8117C15.4493 11.993 14.855 12.4494 13.7755 12.3208C13.2331 12.2561 12.8151 12.0339 12.4839 11.7121C11.9254 12.6375 11.1148 13.2471 10.174 13.3263C7.57382 13.5468 6.26606 11.3715 6.18092 8.95776C4.23461 9.14848 1.57567 10.3456 0.158935 11.0446V33.6816ZM10.1763 4.02459C11.0916 4.0629 11.8843 4.65378 12.4402 5.55882C12.8446 5.12801 13.3938 4.83598 14.1209 4.73381C14.1711 4.727 14.2784 4.72444 14.3661 4.72359C12.871 3.08634 10.7263 2.05359 8.3356 2.05359C7.89798 2.05359 7.47143 2.09786 7.05254 2.16427C6.96569 2.16853 6.85586 2.18385 6.71197 2.21621C6.70601 2.21748 6.70005 2.21855 6.6941 2.21961C6.68814 2.22068 6.68217 2.22174 6.67622 2.22302C6.67134 2.22406 6.66747 2.22468 6.66371 2.22528C6.65828 2.22614 6.65311 2.22696 6.64557 2.22898C4.99469 2.63339 3.75334 2.30475 3.04753 1.34692C2.97346 1.2456 2.67036 0.791806 2.5767 0C2.05649 0.537236 1.71678 1.4261 2.00881 2.3635C2.2421 3.11018 2.66865 3.51715 3.13863 3.74703C2.4243 3.79045 1.82832 3.65678 1.23233 3.20639C0.880705 2.93905 0.665298 2.65127 0.299194 1.89352C-0.0558415 2.41033 -0.0243404 3.00376 0.0412177 3.31622C0.128912 3.72234 0.342616 4.18295 0.612512 4.44518C1.03311 4.85726 1.62824 4.94326 2.10672 4.9407C1.40602 5.76486 0.862823 6.7278 0.531627 7.7861C2.08031 7.4366 4.04739 7.30373 5.49059 7.20625C5.80877 7.18475 6.10149 7.16498 6.35864 7.14499C6.80393 5.32639 8.0623 3.93604 10.1763 4.02459ZM15.8817 8.58029C15.8817 10.0168 15.2124 11.1813 14.3867 11.1813C13.561 11.1813 12.8916 10.0168 12.8916 8.58029C12.8916 7.14377 13.561 5.97925 14.3867 5.97925C15.2124 5.97925 15.8817 7.14377 15.8817 8.58029ZM12.0969 8.70478C12.0969 10.5165 11.2373 11.9852 10.177 11.9852C9.11665 11.9852 8.25708 10.5165 8.25708 8.70478C8.25708 6.89303 9.11665 5.42432 10.177 5.42432C11.2373 5.42432 12.0969 6.89303 12.0969 8.70478Z" fill="#fff"/>
</svg>
</a>
<a href="https://youtube.com/@mempool" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg></a>
<a href="https://bitcointv.com/c/mempool/videos" target="_blank"><svg _ngcontent-serverApp-c90="" xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path _ngcontent-serverApp-c90="" d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="#fff"></path><path _ngcontent-serverApp-c90="" d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path _ngcontent-serverApp-c90="" d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="#fff"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse _ngcontent-serverApp-c90="" fill="#fff" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path _ngcontent-serverApp-c90="" fill="#fff" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse _ngcontent-serverApp-c90="" fill="#fff" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path _ngcontent-serverApp-c90="" fill="#fff" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path _ngcontent-serverApp-c90="" d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path _ngcontent-serverApp-c90="" d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path _ngcontent-serverApp-c90="" fill="#fff" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg></a>
<a href="https://mempool.chat" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg></a>

View File

@@ -500,7 +500,7 @@ html:lang(ru) .card-title {
}
.fee-distribution-chart {
height: 250px;
height: 265px;
}
.fees-wrapper-tooltip-chart {

View File

@@ -361,10 +361,10 @@ BITCOIN_ELECTRS_REPO_NAME=electrs
BITCOIN_ELECTRS_REPO_BRANCH=mempool
BITCOIN_ELECTRS_LATEST_RELEASE=mempool
ELEMENTS_ELECTRS_REPO_URL=https://github.com/blockstream/electrs
ELEMENTS_ELECTRS_REPO_URL=https://github.com/mempool/electrs
ELEMENTS_ELECTRS_REPO_NAME=electrs
ELEMENTS_ELECTRS_REPO_BRANCH=new-index
ELEMENTS_ELECTRS_LATEST_RELEASE=new-index
ELEMENTS_ELECTRS_REPO_BRANCH=mempool
ELEMENTS_ELECTRS_LATEST_RELEASE=mempool
LIQUID_ASSET_REGISTRY_DB_URL=https://github.com/blockstream/asset_registry_db
LIQUID_ASSET_REGISTRY_DB_NAME=asset_registry_db

View File

@@ -21,6 +21,7 @@ do for url in / \
'/api/v1/statistics/2y' \
'/api/v1/statistics/3y' \
'/api/v1/statistics/4y' \
'/api/v1/statistics/all' \
'/api/v1/mining/pools/24h' \
'/api/v1/mining/pools/3d' \
'/api/v1/mining/pools/1w' \