Compare commits
57 Commits
hunicus/ro
...
hunicus/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
247c364ccd | ||
|
|
7b01f54fc6 | ||
|
|
9f2e94d9cd | ||
|
|
824c6f97e4 | ||
|
|
efcb58a4a6 | ||
|
|
9e5d10b15f | ||
|
|
3d33233e51 | ||
|
|
4cd7561af8 | ||
|
|
52c813bcc7 | ||
|
|
c20c7ae11f | ||
|
|
5f582195ad | ||
|
|
a567a28c5c | ||
|
|
59713e2132 | ||
|
|
1e83a99bae | ||
|
|
b5fdb6d64f | ||
|
|
1411b701f9 | ||
|
|
f80257c5ea | ||
|
|
23cd063402 | ||
|
|
2bda12e5f9 | ||
|
|
2e285c8d86 | ||
|
|
70854de6ec | ||
|
|
f5cf5c7cc3 | ||
|
|
d939391e62 | ||
|
|
85e33e689d | ||
|
|
ca1a1228a9 | ||
|
|
d4ed238ae1 | ||
|
|
22baf4186e | ||
|
|
58b8052530 | ||
|
|
9d606d0006 | ||
|
|
d848ab4bef | ||
|
|
eaad63a082 | ||
|
|
ca6ddd609d | ||
|
|
eca40f94c9 | ||
|
|
86f51e3902 | ||
|
|
9f2b98b246 | ||
|
|
e4f3642082 | ||
|
|
48d62a1396 | ||
|
|
d9ed02a033 | ||
|
|
31ae382f75 | ||
|
|
5f787db30d | ||
|
|
da3c39c7d0 | ||
|
|
dbf759fc76 | ||
|
|
e88cf70719 | ||
|
|
1f442b9ea6 | ||
|
|
9ff5ce0d37 | ||
|
|
329c635da5 | ||
|
|
e18f3800be | ||
|
|
9bfc2c9413 | ||
|
|
4fbab08586 | ||
|
|
816f410855 | ||
|
|
73d629d319 | ||
|
|
048399574e | ||
|
|
68e9fb0882 | ||
|
|
50a96ce340 | ||
|
|
38162e1af2 | ||
|
|
b0478f6604 | ||
|
|
6cae3256a9 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.16.0", "18.14.1"]
|
node: ["16", "17", "18"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.16.0", "18.14.1"]
|
node: ["16", "17", "18"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|||||||
@@ -224,7 +224,12 @@ class BitcoinRoutes {
|
|||||||
} else {
|
} else {
|
||||||
let cpfpInfo;
|
let cpfpInfo;
|
||||||
if (config.DATABASE.ENABLED) {
|
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) {
|
if (cpfpInfo) {
|
||||||
res.json(cpfpInfo);
|
res.json(cpfpInfo);
|
||||||
@@ -394,9 +399,13 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
private async getBlockAuditSummary(req: Request, res: Response) {
|
private async getBlockAuditSummary(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
|
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
if (auditSummary) {
|
||||||
res.json(transactions);
|
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) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
|
||||||
block.tx.forEach(tx => {
|
block.tx.forEach(tx => {
|
||||||
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
|
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 cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.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}`);
|
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
// start async callbacks
|
// start async callbacks
|
||||||
@@ -668,12 +675,13 @@ class Blocks {
|
|||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||||
this.updateTimerProgress(timer, `reindexed block`);
|
this.updateTimerProgress(timer, `reindexed block`);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
let cpfpSummary;
|
||||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
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`);
|
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 mining.$indexDifficultyAdjustments();
|
||||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
@@ -704,7 +712,7 @@ class Blocks {
|
|||||||
|
|
||||||
// Save blocks summary for visualization if it's enabled
|
// Save blocks summary for visualization if it's enabled
|
||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
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}`);
|
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
@@ -730,6 +738,11 @@ class Blocks {
|
|||||||
this.currentDifficulty = block.difficulty;
|
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);
|
this.blocks.push(blockExtended);
|
||||||
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-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();
|
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++;
|
handledBlocks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,7 +835,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
|
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
|
||||||
skipDBLookup = false): Promise<TransactionStripped[]>
|
skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionStripped[]>
|
||||||
{
|
{
|
||||||
if (skipMemoryCache === false) {
|
if (skipMemoryCache === false) {
|
||||||
// Check the memory cache
|
// Check the memory cache
|
||||||
@@ -845,13 +853,35 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call Core RPC
|
let height = blockHeight;
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
let summary: BlockSummary;
|
||||||
const summary = this.summarizeBlock(block);
|
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
|
// Index the response if needed
|
||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
|
await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary.transactions;
|
return summary.transactions;
|
||||||
@@ -1007,19 +1037,11 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
||||||
let summary;
|
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
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 {
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
@@ -1050,9 +1072,13 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
||||||
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
|
try {
|
||||||
if (!result) {
|
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
|
||||||
await cpfpRepository.$insertProgressMarker(height);
|
if (!result) {
|
||||||
|
await cpfpRepository.$insertProgressMarker(height);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// not a fatal error, we'll try again next time the indexer runs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ export class Common {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] {
|
||||||
|
return txs.map(this.stripTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
static sleep$(ms: number): Promise<void> {
|
static sleep$(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class MempoolBlocks {
|
|||||||
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
|
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
|
||||||
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||||
onlineStats = true;
|
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);
|
feeStatsCalculator.processNext(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +334,7 @@ class MempoolBlocks {
|
|||||||
if (hasBlockStack) {
|
if (hasBlockStack) {
|
||||||
stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
|
stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
|
||||||
hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
|
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 }[] = [];
|
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = [];
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let loggerTimer = new Date().getTime() / 1000;
|
let intervalTimer = Date.now();
|
||||||
for (const txid of transactions) {
|
for (const txid of transactions) {
|
||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
try {
|
try {
|
||||||
@@ -179,18 +179,20 @@ class Mempool {
|
|||||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
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) {
|
if (Date.now() - intervalTimer > 5_000) {
|
||||||
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
|
||||||
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
if (this.inSync) {
|
||||||
loadingIndicators.setProgress('mempool', progress);
|
// Break and restart mempool loop if we spend too much time processing
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
// new transactions that may lead to falling behind on block height
|
||||||
}
|
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
|
||||||
// Break and restart mempool loop if we spend too much time processing
|
break;
|
||||||
// new transactions that may lead to falling behind on block height
|
} else {
|
||||||
if (this.inSync && (new Date().getTime()) - start > 10_000) {
|
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
|
||||||
logger.debug('Breaking mempool loop because the 10s time limit exceeded.');
|
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
|
||||||
break;
|
loadingIndicators.setProgress('mempool', progress);
|
||||||
|
intervalTimer = Date.now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class Mining {
|
|||||||
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
|
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
|
||||||
slug: poolInfo.slug,
|
slug: poolInfo.slug,
|
||||||
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
|
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
|
||||||
|
avgFeeDelta: poolInfo.avgFeeDelta,
|
||||||
};
|
};
|
||||||
poolsStats.push(poolStat);
|
poolsStats.push(poolStat);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class StatisticsApi {
|
|||||||
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
|
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
|
||||||
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
|
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
|
||||||
FROM statistics \
|
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} \
|
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||||
ORDER BY statistics.added DESC;`;
|
ORDER BY statistics.added DESC;`;
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ class StatisticsApi {
|
|||||||
vsize_1800,
|
vsize_1800,
|
||||||
vsize_2000 \
|
vsize_2000 \
|
||||||
FROM statistics \
|
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} \
|
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||||
ORDER BY statistics.added DESC;`;
|
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[] {
|
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
|
||||||
return statistic.map((s) => {
|
return statistic.map((s) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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/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/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/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('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
@@ -26,10 +27,6 @@ class StatisticsRoutes {
|
|||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
switch (time as string) {
|
switch (time as string) {
|
||||||
case '2h':
|
|
||||||
result = await statisticsApi.$list2H();
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
|
||||||
break;
|
|
||||||
case '24h':
|
case '24h':
|
||||||
result = await statisticsApi.$list24H();
|
result = await statisticsApi.$list24H();
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
@@ -58,8 +55,13 @@ class StatisticsRoutes {
|
|||||||
case '4y':
|
case '4y':
|
||||||
result = await statisticsApi.$list4Y();
|
result = await statisticsApi.$list4Y();
|
||||||
break;
|
break;
|
||||||
|
case 'all':
|
||||||
|
result = await statisticsApi.$listAll();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result = await statisticsApi.$list2H();
|
result = await statisticsApi.$list2H();
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ import { deepClone } from '../utils/clone';
|
|||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
|
|
||||||
|
// valid 'want' subscriptions
|
||||||
|
const wantable = [
|
||||||
|
'blocks',
|
||||||
|
'mempool-blocks',
|
||||||
|
'live-2h-chart',
|
||||||
|
'stats',
|
||||||
|
];
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
private extraInitProperties = {};
|
private extraInitProperties = {};
|
||||||
@@ -30,7 +38,7 @@ class WebsocketHandler {
|
|||||||
private numConnected = 0;
|
private numConnected = 0;
|
||||||
private numDisconnected = 0;
|
private numDisconnected = 0;
|
||||||
|
|
||||||
private initData: { [key: string]: string } = {};
|
private socketData: { [key: string]: string } = {};
|
||||||
private serializedInitData: string = '{}';
|
private serializedInitData: string = '{}';
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -39,28 +47,28 @@ class WebsocketHandler {
|
|||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtraInitProperties(property: string, value: any) {
|
setExtraInitData(property: string, value: any) {
|
||||||
this.extraInitProperties[property] = value;
|
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)) {
|
for (const property of Object.keys(data)) {
|
||||||
if (data[property] != null) {
|
if (data[property] != null) {
|
||||||
this.initData[property] = JSON.stringify(data[property]);
|
this.socketData[property] = JSON.stringify(data[property]);
|
||||||
} else {
|
} else {
|
||||||
delete this.initData[property];
|
delete this.socketData[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.serializedInitData = '{'
|
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 _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
this.setInitDataFields({
|
this.updateSocketDataFields({
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
'blocks': _blocks,
|
'blocks': _blocks,
|
||||||
@@ -94,11 +102,33 @@ class WebsocketHandler {
|
|||||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||||
const response = {};
|
const response = {};
|
||||||
|
|
||||||
if (parsedMessage.action === 'want') {
|
const wantNow = {};
|
||||||
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
if (parsedMessage && parsedMessage.action === 'want' && Array.isArray(parsedMessage.data)) {
|
||||||
client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
|
for (const sub of wantable) {
|
||||||
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
const key = `want-${sub}`;
|
||||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
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']) {
|
if (parsedMessage && parsedMessage['track-tx']) {
|
||||||
@@ -109,21 +139,21 @@ class WebsocketHandler {
|
|||||||
if (parsedMessage['watch-mempool']) {
|
if (parsedMessage['watch-mempool']) {
|
||||||
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
|
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
|
||||||
if (rbfCacheTxid) {
|
if (rbfCacheTxid) {
|
||||||
response['txReplaced'] = {
|
response['txReplaced'] = JSON.stringify({
|
||||||
txid: rbfCacheTxid,
|
txid: rbfCacheTxid,
|
||||||
};
|
});
|
||||||
client['track-tx'] = null;
|
client['track-tx'] = null;
|
||||||
} else {
|
} else {
|
||||||
// It might have appeared before we had the time to start watching for it
|
// It might have appeared before we had the time to start watching for it
|
||||||
const tx = memPool.getMempool()[trackTxid];
|
const tx = memPool.getMempool()[trackTxid];
|
||||||
if (tx) {
|
if (tx) {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
response['tx'] = tx;
|
response['tx'] = JSON.stringify(tx);
|
||||||
} else {
|
} else {
|
||||||
// tx.prevout is missing from transactions when in bitcoind mode
|
// tx.prevout is missing from transactions when in bitcoind mode
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = JSON.stringify(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@@ -131,7 +161,7 @@ class WebsocketHandler {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = JSON.stringify(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||||
@@ -141,10 +171,10 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
const tx = memPool.getMempool()[trackTxid];
|
const tx = memPool.getMempool()[trackTxid];
|
||||||
if (tx && tx.position) {
|
if (tx && tx.position) {
|
||||||
response['txPosition'] = {
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position: tx.position,
|
position: tx.position,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client['track-tx'] = null;
|
client['track-tx'] = null;
|
||||||
@@ -177,10 +207,10 @@ class WebsocketHandler {
|
|||||||
const index = parsedMessage['track-mempool-block'];
|
const index = parsedMessage['track-mempool-block'];
|
||||||
client['track-mempool-block'] = index;
|
client['track-mempool-block'] = index;
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
response['projected-block-transactions'] = {
|
response['projected-block-transactions'] = JSON.stringify({
|
||||||
index: index,
|
index: index,
|
||||||
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
||||||
};
|
});
|
||||||
} else {
|
} else {
|
||||||
client['track-mempool-block'] = null;
|
client['track-mempool-block'] = null;
|
||||||
}
|
}
|
||||||
@@ -189,23 +219,24 @@ class WebsocketHandler {
|
|||||||
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
|
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
|
||||||
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
|
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
|
||||||
client['track-rbf'] = parsedMessage['track-rbf'];
|
client['track-rbf'] = parsedMessage['track-rbf'];
|
||||||
|
response['rbfLatest'] = JSON.stringify(rbfCache.getRbfTrees(parsedMessage['track-rbf'] === 'fullRbf'));
|
||||||
} else {
|
} else {
|
||||||
client['track-rbf'] = false;
|
client['track-rbf'] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
if (!this.initData['blocks']?.length || !this.initData['da']) {
|
if (!this.socketData['blocks']?.length || !this.socketData['da']) {
|
||||||
this.updateInitData();
|
this.updateSocketData();
|
||||||
}
|
}
|
||||||
if (!this.initData['blocks']?.length) {
|
if (!this.socketData['blocks']?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.send(this.serializedInitData);
|
client.send(this.serializedInitData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'ping') {
|
if (parsedMessage.action === 'ping') {
|
||||||
response['pong'] = true;
|
response['pong'] = JSON.stringify(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
|
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
|
||||||
@@ -221,7 +252,8 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(JSON.stringify(response));
|
const serializedResponse = this.serializeResponse(response);
|
||||||
|
client.send(serializedResponse);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : 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');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setInitDataFields({ 'loadingIndicators': indicators });
|
this.updateSocketDataFields({ 'loadingIndicators': indicators });
|
||||||
|
|
||||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
@@ -266,7 +298,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setInitDataFields({ 'conversions': conversionRates });
|
this.updateSocketDataFields({ 'conversions': conversionRates });
|
||||||
|
|
||||||
const response = JSON.stringify({ conversions: conversionRates });
|
const response = JSON.stringify({ conversions: conversionRates });
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
@@ -336,11 +368,21 @@ class WebsocketHandler {
|
|||||||
memPool.addToSpendMap(newTransactions);
|
memPool.addToSpendMap(newTransactions);
|
||||||
const recommendedFees = feeApi.getRecommendedFee();
|
const recommendedFees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
|
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||||
|
|
||||||
// update init data
|
// 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
|
// 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 {
|
function getCachedResponse(key: string, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
responseCache[key] = JSON.stringify(data);
|
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) => {
|
this.wss.clients.forEach(async (client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@@ -490,7 +530,7 @@ class WebsocketHandler {
|
|||||||
if (rbfReplacedBy) {
|
if (rbfReplacedBy) {
|
||||||
response['rbfTransaction'] = JSON.stringify({
|
response['rbfTransaction'] = JSON.stringify({
|
||||||
txid: rbfReplacedBy,
|
txid: rbfReplacedBy,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rbfChange = rbfChanges.map[client['track-tx']];
|
const rbfChange = rbfChanges.map[client['track-tx']];
|
||||||
@@ -524,9 +564,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
const serializedResponse = '{'
|
const serializedResponse = this.serializeResponse(response);
|
||||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
|
||||||
+ '}';
|
|
||||||
client.send(serializedResponse);
|
client.send(serializedResponse);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -562,14 +600,7 @@ class WebsocketHandler {
|
|||||||
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||||
return {
|
|
||||||
txid: tx.txid,
|
|
||||||
vsize: tx.vsize,
|
|
||||||
fee: tx.fee ? Math.round(tx.fee) : 0,
|
|
||||||
value: tx.value,
|
|
||||||
};
|
|
||||||
}) : [];
|
|
||||||
|
|
||||||
let totalFees = 0;
|
let totalFees = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
@@ -633,11 +664,19 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const fees = feeApi.getRecommendedFee();
|
const fees = feeApi.getRecommendedFee();
|
||||||
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
|
|
||||||
// update init data
|
// 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 {
|
function getCachedResponse(key, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
responseCache[key] = JSON.stringify(data);
|
responseCache[key] = JSON.stringify(data);
|
||||||
@@ -645,22 +684,26 @@ class WebsocketHandler {
|
|||||||
return responseCache[key];
|
return responseCache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client['want-blocks']) {
|
const response = {};
|
||||||
return;
|
|
||||||
|
if (client['want-blocks']) {
|
||||||
|
response['block'] = getCachedResponse('block', block);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {};
|
if (client['want-stats']) {
|
||||||
response['block'] = getCachedResponse('block', block);
|
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
|
||||||
response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
|
response['fees'] = getCachedResponse('fees', fees);
|
||||||
response['fees'] = getCachedResponse('fees', fees);
|
|
||||||
|
if (da?.previousTime) {
|
||||||
|
response['da'] = getCachedResponse('da', da);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mBlocks && client['want-mempool-blocks']) {
|
if (mBlocks && client['want-mempool-blocks']) {
|
||||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
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(', ')
|
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||||
+ '}';
|
+ '}';
|
||||||
client.send(serializedResponse);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private printLogs(): void {
|
private printLogs(): void {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
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();
|
this.checkDBFlag();
|
||||||
let hardTimeout;
|
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')}`));
|
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);
|
}, 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[]]>;
|
return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
resolve(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() {
|
public async checkDbConnection() {
|
||||||
this.checkDBFlag();
|
this.checkDBFlag();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class Server {
|
|||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
bisq.startBisqService();
|
bisq.startBisqService();
|
||||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
|
||||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||||
bisqMarkets.startBisqService();
|
bisqMarkets.startBisqService();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface PoolInfo {
|
|||||||
blockCount: number;
|
blockCount: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
avgMatchRate: number | null;
|
avgMatchRate: number | null;
|
||||||
|
avgFeeDelta: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PoolStats extends PoolInfo {
|
export interface PoolStats extends PoolInfo {
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class BlocksAuditRepositories {
|
|||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
||||||
blocks.weight, blocks.tx_count,
|
blocks.weight, blocks.tx_count,
|
||||||
transactions,
|
|
||||||
template,
|
template,
|
||||||
missing_txs as missingTxs,
|
missing_txs as missingTxs,
|
||||||
added_txs as addedTxs,
|
added_txs as addedTxs,
|
||||||
@@ -76,7 +75,6 @@ class BlocksAuditRepositories {
|
|||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||||
JOIN blocks_templates ON blocks_templates.id = 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}"
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -85,12 +83,9 @@ class BlocksAuditRepositories {
|
|||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
|
||||||
rows[0].template = JSON.parse(rows[0].template);
|
rows[0].template = JSON.parse(rows[0].template);
|
||||||
|
|
||||||
if (rows[0].transactions.length) {
|
return rows[0];
|
||||||
return rows[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -5,52 +5,10 @@ import { Ancestor, CpfpCluster } from '../mempool.interfaces';
|
|||||||
import transactionRepository from '../repositories/TransactionRepository';
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
|
|
||||||
class CpfpRepository {
|
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> {
|
public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const clusterValues: any[] = [];
|
const clusterValues: [string, number, Buffer, number][] = [];
|
||||||
const txs: any[] = [];
|
const txs: { txid: string, cluster: string }[] = [];
|
||||||
|
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
if (cluster.txs?.length) {
|
if (cluster.txs?.length) {
|
||||||
@@ -76,16 +34,10 @@ class CpfpRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queries: { query, params }[] = [];
|
||||||
|
|
||||||
const maxChunk = 100;
|
const maxChunk = 100;
|
||||||
let chunkIndex = 0;
|
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
|
// insert clusters in batches of up to 100 rows
|
||||||
while (chunkIndex < clusterValues.length) {
|
while (chunkIndex < clusterValues.length) {
|
||||||
const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
|
const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
|
||||||
@@ -97,12 +49,23 @@ class CpfpRepository {
|
|||||||
return (' (UNHEX(?), ?, ?, ?)');
|
return (' (UNHEX(?), ?, ?, ?)');
|
||||||
}) + ';';
|
}) + ';';
|
||||||
const values = chunk.flat();
|
const values = chunk.flat();
|
||||||
await DB.query(
|
queries.push({
|
||||||
query,
|
query,
|
||||||
values
|
params: values,
|
||||||
);
|
});
|
||||||
chunkIndex += maxChunk;
|
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;
|
return true;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
@@ -120,8 +83,8 @@ class CpfpRepository {
|
|||||||
[clusterRoot]
|
[clusterRoot]
|
||||||
);
|
);
|
||||||
const cluster = clusterRows[0];
|
const cluster = clusterRows[0];
|
||||||
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
|
||||||
if (cluster?.txs) {
|
if (cluster?.txs) {
|
||||||
|
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
||||||
cluster.txs = this.unpack(cluster.txs);
|
cluster.txs = this.unpack(cluster.txs);
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class PoolsRepository {
|
|||||||
pools.name AS name,
|
pools.name AS name,
|
||||||
pools.link AS link,
|
pools.link AS link,
|
||||||
slug,
|
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
|
FROM blocks
|
||||||
JOIN pools on pools.id = pool_id
|
JOIN pools on pools.id = pool_id
|
||||||
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
|
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ class TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $batchSetCluster(txs): Promise<void> {
|
public buildBatchSetQuery(txs: { txid: string, cluster: string }[]): { query, params } {
|
||||||
try {
|
let query = `
|
||||||
let query = `
|
|
||||||
INSERT IGNORE INTO compact_transactions
|
INSERT IGNORE INTO compact_transactions
|
||||||
(
|
(
|
||||||
txid,
|
txid,
|
||||||
@@ -35,13 +34,22 @@ class TransactionRepository {
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
`;
|
`;
|
||||||
query += txs.map(tx => {
|
query += txs.map(tx => {
|
||||||
return (' (UNHEX(?), UNHEX(?))');
|
return (' (UNHEX(?), UNHEX(?))');
|
||||||
}) + ';';
|
}) + ';';
|
||||||
const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
|
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(
|
await DB.query(
|
||||||
query,
|
query.query,
|
||||||
values
|
query.params,
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
|||||||
3
contributors/0xflicker.txt
Normal file
3
contributors/0xflicker.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 24, 2023.
|
||||||
|
|
||||||
|
Signed: 0xflicker
|
||||||
@@ -173,6 +173,21 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -205,7 +220,7 @@
|
|||||||
<img class="image" src="/resources/profile/mynodebtc.png" />
|
<img class="image" src="/resources/profile/mynodebtc.png" />
|
||||||
<span>myNode</span>
|
<span>myNode</span>
|
||||||
</a>
|
</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" />
|
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||||
<span>RoninDojo</span>
|
<span>RoninDojo</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
||||||
>{{ blockAudit?.matchRate }}%</span>
|
>{{ blockAudit?.matchRate }}%</span>
|
||||||
<ng-template #nullHealth>
|
<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>
|
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
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 { 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 { StateService } from '../../services/state.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
strippedTransactions: TransactionStripped[];
|
strippedTransactions: TransactionStripped[];
|
||||||
overviewTransitionDirection: string;
|
overviewTransitionDirection: string;
|
||||||
isLoadingOverview = true;
|
isLoadingOverview = true;
|
||||||
isLoadingAudit = true;
|
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
@@ -281,143 +280,111 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.auditSupported) {
|
this.overviewSubscription = block$.pipe(
|
||||||
this.overviewSubscription = block$.pipe(
|
switchMap((block) => {
|
||||||
startWith(null),
|
return forkJoin([
|
||||||
pairwise(),
|
this.apiService.getStrippedBlockTransactions$(block.id)
|
||||||
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)
|
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.overviewError = err;
|
this.overviewError = err;
|
||||||
this.isLoadingAudit = false;
|
return of(null);
|
||||||
return of([]);
|
|
||||||
})
|
})
|
||||||
);
|
),
|
||||||
}
|
!this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
|
||||||
),
|
.pipe(
|
||||||
filter((response) => response != null),
|
catchError((err) => {
|
||||||
map((response) => {
|
this.overviewError = err;
|
||||||
const blockAudit = response.body;
|
return of(null);
|
||||||
const inTemplate = {};
|
})
|
||||||
const inBlock = {};
|
)
|
||||||
const isAdded = {};
|
]);
|
||||||
const isCensored = {};
|
})
|
||||||
const isMissing = {};
|
)
|
||||||
const isSelected = {};
|
.subscribe(([transactions, blockAudit]) => {
|
||||||
const isFresh = {};
|
if (transactions) {
|
||||||
const isSigop = {};
|
this.strippedTransactions = transactions;
|
||||||
this.numMissing = 0;
|
} else {
|
||||||
this.numUnexpected = 0;
|
this.strippedTransactions = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (blockAudit?.template) {
|
this.blockAudit = null;
|
||||||
for (const tx of blockAudit.template) {
|
if (transactions && blockAudit) {
|
||||||
inTemplate[tx.txid] = true;
|
const inTemplate = {};
|
||||||
}
|
const inBlock = {};
|
||||||
for (const tx of blockAudit.transactions) {
|
const isAdded = {};
|
||||||
inBlock[tx.txid] = true;
|
const isCensored = {};
|
||||||
}
|
const isMissing = {};
|
||||||
for (const txid of blockAudit.addedTxs) {
|
const isSelected = {};
|
||||||
isAdded[txid] = true;
|
const isFresh = {};
|
||||||
}
|
const isSigop = {};
|
||||||
for (const txid of blockAudit.missingTxs) {
|
this.numMissing = 0;
|
||||||
isCensored[txid] = true;
|
this.numUnexpected = 0;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
|
if (blockAudit?.template) {
|
||||||
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
|
for (const tx of blockAudit.template) {
|
||||||
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
|
inTemplate[tx.txid] = true;
|
||||||
|
|
||||||
this.setAuditAvailable(true);
|
|
||||||
} else {
|
|
||||||
this.setAuditAvailable(false);
|
|
||||||
}
|
}
|
||||||
return blockAudit;
|
for (const tx of transactions) {
|
||||||
}),
|
inBlock[tx.txid] = true;
|
||||||
catchError((err) => {
|
}
|
||||||
console.log(err);
|
for (const txid of blockAudit.addedTxs) {
|
||||||
this.error = err;
|
isAdded[txid] = true;
|
||||||
this.isLoadingOverview = false;
|
}
|
||||||
this.isLoadingAudit = false;
|
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);
|
this.setAuditAvailable(false);
|
||||||
return of(null);
|
}
|
||||||
}),
|
} else {
|
||||||
).subscribe((blockAudit) => {
|
this.setAuditAvailable(false);
|
||||||
this.blockAudit = blockAudit;
|
}
|
||||||
this.setupBlockGraphs();
|
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
this.isLoadingAudit = false;
|
this.setupBlockGraphs();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
@@ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
||||||
if (!this.auditSupported) {
|
if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
|
||||||
this.setAuditAvailable(false);
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
|
||||||
|
if (!this.auditSupported) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
switch (this.stateService.network) {
|
switch (this.stateService.network) {
|
||||||
case 'testnet':
|
case 'testnet':
|
||||||
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinBlockFee(block: BlockExtended): number {
|
getMinBlockFee(block: BlockExtended): number {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.ltr-transition]="ltrTransitionEnabled" #container>
|
<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>
|
<span>
|
||||||
<div class="blocks-wrapper">
|
<div class="blocks-wrapper">
|
||||||
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>
|
<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>
|
<app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks>
|
||||||
<ng-container *ngFor="let page of pages; trackBy: trackByPageFn">
|
<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>
|
<app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage" [loadingTip]="loadingTip" [connected]="connected"></app-blockchain-blocks>
|
||||||
|
|||||||
@@ -26,43 +26,14 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 75px;
|
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 {
|
.blockchain-wrapper.time-ltr {
|
||||||
transform: translateX(420px);
|
.position-container {
|
||||||
}
|
transform: translateX(calc(100vw - var(--divider-offset) - var(--mempool-offset)));
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { firstValueFrom, Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
|
||||||
@@ -13,43 +13,95 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
|||||||
@Input() pageIndex: number;
|
@Input() pageIndex: number;
|
||||||
@Input() blocksPerPage: number = 8;
|
@Input() blocksPerPage: number = 8;
|
||||||
@Input() minScrollWidth: number = 0;
|
@Input() minScrollWidth: number = 0;
|
||||||
|
@Input() scrollableMempool: boolean = false;
|
||||||
|
|
||||||
|
@Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
network: string;
|
network: string;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||||
ltrTransitionEnabled = false;
|
ltrTransitionEnabled = false;
|
||||||
|
flipping = false;
|
||||||
connectionStateSubscription: Subscription;
|
connectionStateSubscription: Subscription;
|
||||||
loadingTip: boolean = true;
|
loadingTip: boolean = true;
|
||||||
connected: boolean = true;
|
connected: boolean = true;
|
||||||
|
|
||||||
|
dividerOffset: number = 0;
|
||||||
|
mempoolOffset: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
|
this.onResize();
|
||||||
this.network = this.stateService.network;
|
this.network = this.stateService.network;
|
||||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
this.timeLtr = !!ltr;
|
this.timeLtr = !!ltr;
|
||||||
});
|
});
|
||||||
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
|
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
|
||||||
this.connected = (state === 2);
|
this.connected = (state === 2);
|
||||||
})
|
});
|
||||||
firstValueFrom(this.stateService.chainTip$).then(tip => {
|
firstValueFrom(this.stateService.chainTip$).then(() => {
|
||||||
this.loadingTip = false;
|
this.loadingTip = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.connectionStateSubscription.unsubscribe();
|
this.connectionStateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByPageFn(index: number, item: { index: number }) {
|
trackByPageFn(index: number, item: { index: number }): number {
|
||||||
return item.index;
|
return item.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTimeDirection() {
|
toggleTimeDirection(): void {
|
||||||
this.ltrTransitionEnabled = true;
|
this.ltrTransitionEnabled = false;
|
||||||
this.stateService.timeLtr.next(!this.timeLtr);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { OnChanges } from '@angular/core';
|
import { OnChanges } from '@angular/core';
|
||||||
import { Component, Input, OnInit, ChangeDetectionStrategy } 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({
|
@Component({
|
||||||
selector: 'app-fee-distribution-graph',
|
selector: 'app-fee-distribution-graph',
|
||||||
@@ -7,47 +11,121 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
|
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
|
||||||
@Input() data: any;
|
@Input() feeRange: number[];
|
||||||
|
@Input() vsize: number;
|
||||||
|
@Input() transactions: TransactionStripped[];
|
||||||
@Input() height: number | string = 210;
|
@Input() height: number | string = 210;
|
||||||
@Input() top: number | string = 20;
|
@Input() top: number | string = 20;
|
||||||
@Input() right: number | string = 22;
|
@Input() right: number | string = 22;
|
||||||
@Input() left: number | string = 30;
|
@Input() left: number | string = 30;
|
||||||
|
@Input() numSamples: number = 200;
|
||||||
|
@Input() numLabels: number = 10;
|
||||||
|
|
||||||
|
simple: boolean = false;
|
||||||
|
data: number[][];
|
||||||
|
labelInterval: number = 50;
|
||||||
|
|
||||||
mempoolVsizeFeesOptions: any;
|
mempoolVsizeFeesOptions: any;
|
||||||
mempoolVsizeFeesInitOptions = {
|
mempoolVsizeFeesInitOptions = {
|
||||||
renderer: 'svg'
|
renderer: 'svg'
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() { }
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
private vbytesPipe: VbytesPipe,
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.mountChart();
|
this.mountChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges(): void {
|
||||||
|
this.simple = !!this.feeRange?.length;
|
||||||
|
this.prepareChart();
|
||||||
this.mountChart();
|
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 = {
|
this.mempoolVsizeFeesOptions = {
|
||||||
grid: {
|
grid: {
|
||||||
height: '210',
|
height: '210',
|
||||||
right: '20',
|
right: '20',
|
||||||
top: '22',
|
top: '22',
|
||||||
left: '30',
|
left: '40',
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
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: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
// name: 'Effective Fee Rate s/vb',
|
||||||
|
// nameLocation: 'middle',
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
type: 'dotted',
|
type: 'dotted',
|
||||||
color: '#ffffff66',
|
color: '#ffffff66',
|
||||||
opacity: 0.25,
|
opacity: 0.25,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (value: number): string => {
|
||||||
|
const selectedPowerOfTen = selectPowerOfTen(value);
|
||||||
|
const newVal = Math.round(value / selectedPowerOfTen.divider);
|
||||||
|
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
@@ -58,14 +136,18 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges {
|
|||||||
position: 'top',
|
position: 'top',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
textShadowBlur: 0,
|
textShadowBlur: 0,
|
||||||
formatter: (label: any) => {
|
formatter: (label: { data: number[] }): string => {
|
||||||
return Math.floor(label.data);
|
const value = label.data[1];
|
||||||
|
const selectedPowerOfTen = selectPowerOfTen(value);
|
||||||
|
const newVal = Math.round(value / selectedPowerOfTen.divider);
|
||||||
|
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
showAllSymbol: false,
|
||||||
smooth: true,
|
smooth: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#D81B60',
|
color: '#D81B60',
|
||||||
width: 4,
|
width: 1,
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#b71c1c',
|
color: '#b71c1c',
|
||||||
|
|||||||
@@ -39,11 +39,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
<div class="col-md chart-container">
|
<div class="col-md chart-container">
|
||||||
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
mempoolBlockIndex: number;
|
mempoolBlockIndex: number;
|
||||||
mempoolBlock$: Observable<MempoolBlock>;
|
mempoolBlock$: Observable<MempoolBlock>;
|
||||||
|
mempoolBlockTransactions$: Observable<TransactionStripped[]>;
|
||||||
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
|
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
|
||||||
previewTx: TransactionStripped | void;
|
previewTx: TransactionStripped | void;
|
||||||
webGlEnabled: boolean;
|
webGlEnabled: boolean;
|
||||||
@@ -53,6 +54,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
|
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
|
||||||
this.ordinal$.next(ordinal);
|
this.ordinal$.next(ordinal);
|
||||||
this.seoService.setTitle(ordinal);
|
this.seoService.setTitle(ordinal);
|
||||||
|
mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize;
|
||||||
return mempoolBlocks[this.mempoolBlockIndex];
|
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$;
|
this.network$ = this.stateService.networkChanged$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Router } from '@angular/router';
|
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 { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
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() count: number = null;
|
||||||
@Input() spotlight: number = 0;
|
@Input() spotlight: number = 0;
|
||||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||||
|
@Input() allBlocks: boolean = false;
|
||||||
|
|
||||||
|
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
specialBlocks = specialBlocks;
|
specialBlocks = specialBlocks;
|
||||||
mempoolBlocks: MempoolBlock[] = [];
|
mempoolBlocks: MempoolBlock[] = [];
|
||||||
@@ -143,16 +146,24 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
||||||
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
||||||
|
|
||||||
|
this.now = Date.now();
|
||||||
|
|
||||||
this.updateMempoolBlockStyles();
|
this.updateMempoolBlockStyles();
|
||||||
this.calculateTransactionPosition();
|
this.calculateTransactionPosition();
|
||||||
|
|
||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.cd.markForCheck();
|
||||||
|
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((da) => {
|
map((da) => {
|
||||||
this.now = new Date().getTime();
|
this.now = Date.now();
|
||||||
|
this.cd.markForCheck();
|
||||||
return da;
|
return da;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -254,7 +265,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
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) {
|
while (blocks.length < blocksAmount) {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
blockSize: 0,
|
blockSize: 0,
|
||||||
@@ -274,10 +288,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
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) {
|
if (this.count) {
|
||||||
blocksAmount = 8;
|
blocksAmount = 8;
|
||||||
} else {
|
} else if (!this.allBlocks) {
|
||||||
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||||
}
|
}
|
||||||
while (blocks.length > blocksAmount) {
|
while (blocks.length > blocksAmount) {
|
||||||
|
|||||||
@@ -94,7 +94,8 @@
|
|||||||
<th class="" i18n="master-page.blocks">Blocks</th>
|
<th class="" i18n="master-page.blocks">Blocks</th>
|
||||||
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
|
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
|
||||||
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
||||||
<th 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
||||||
@@ -121,7 +122,15 @@
|
|||||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</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>
|
||||||
<tr style="border-top: 1px solid #555">
|
<tr style="border-top: 1px solid #555">
|
||||||
<td class="d-none d-md-table-cell"></td>
|
<td class="d-none d-md-table-cell"></td>
|
||||||
|
|||||||
@@ -110,4 +110,15 @@
|
|||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
.difference {
|
||||||
|
&.positive {
|
||||||
|
color: rgb(66, 183, 71);
|
||||||
|
}
|
||||||
|
&.negative {
|
||||||
|
color: rgb(183, 66, 66);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
(dragstart)="onDragStart($event)"
|
(dragstart)="onDragStart($event)"
|
||||||
(scroll)="onScroll($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>
|
||||||
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
|
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
|
||||||
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -24,6 +24,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
chainTipSubscription: Subscription;
|
chainTipSubscription: Subscription;
|
||||||
chainTip: number = -1;
|
chainTip: number = -1;
|
||||||
tipIsSet: boolean = false;
|
tipIsSet: boolean = false;
|
||||||
|
lastMark: MarkBlockState;
|
||||||
markBlockSubscription: Subscription;
|
markBlockSubscription: Subscription;
|
||||||
blockCounterSubscription: Subscription;
|
blockCounterSubscription: Subscription;
|
||||||
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
|
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
|
||||||
@@ -40,10 +41,11 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
minScrollWidth: number;
|
minScrollWidth: number;
|
||||||
pageIndex: number = 0;
|
pageIndex: number = 0;
|
||||||
pages: any[] = [];
|
pages: any[] = [];
|
||||||
pendingMark: number | void = null;
|
pendingMark: number | null = null;
|
||||||
lastUpdate: number = 0;
|
lastUpdate: number = 0;
|
||||||
lastMouseX: number;
|
lastMouseX: number;
|
||||||
velocity: number = 0;
|
velocity: number = 0;
|
||||||
|
mempoolOffset: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@@ -70,19 +72,40 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
this.chainTip = height;
|
this.chainTip = height;
|
||||||
this.tipIsSet = true;
|
this.tipIsSet = true;
|
||||||
this.updatePages();
|
this.updatePages();
|
||||||
if (this.pendingMark != null) {
|
this.applyPendingMarkArrow();
|
||||||
this.scrollToBlock(this.pendingMark);
|
|
||||||
this.pendingMark = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => {
|
this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => {
|
||||||
|
let blockHeight;
|
||||||
|
let newMark = true;
|
||||||
if (mark?.blockHeight != null) {
|
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.tipIsSet) {
|
||||||
if (!this.blockInViewport(mark.blockHeight)) {
|
let scrollToHeight = blockHeight;
|
||||||
this.scrollToBlock(mark.blockHeight);
|
if (blockHeight < 0) {
|
||||||
|
scrollToHeight = this.chainTip - blockHeight;
|
||||||
}
|
}
|
||||||
} else {
|
if (newMark && !this.blockInViewport(scrollToHeight)) {
|
||||||
this.pendingMark = mark.blockHeight;
|
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'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize(): void {
|
onResize(): void {
|
||||||
this.isMobile = window.innerWidth <= 767.98;
|
this.isMobile = window.innerWidth <= 767.98;
|
||||||
@@ -350,7 +391,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
resetScroll(): void {
|
resetScroll(): void {
|
||||||
this.scrollToBlock(this.chainTip);
|
this.scrollToBlock(this.chainTip);
|
||||||
this.blockchainContainer.nativeElement.scrollLeft = 0;
|
this.setScrollLeft(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageIndexOf(height: number): number {
|
getPageIndexOf(height: number): number {
|
||||||
@@ -368,9 +409,17 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
getConvertedScrollOffset(): number {
|
getConvertedScrollOffset(): number {
|
||||||
if (this.timeLtr) {
|
if (this.timeLtr) {
|
||||||
return -this.blockchainContainer?.nativeElement?.scrollLeft || 0;
|
return -(this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset;
|
||||||
} else {
|
} 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() {
|
ngOnDestroy() {
|
||||||
if (this.blockchainContainer?.nativeElement) {
|
if (this.blockchainContainer?.nativeElement) {
|
||||||
// clean up scroll position to prevent caching wrong scroll in Firefox
|
// clean up scroll position to prevent caching wrong scroll in Firefox
|
||||||
this.blockchainContainer.nativeElement.scrollLeft = 0;
|
this.setScrollLeft(0);
|
||||||
}
|
}
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.chainTipSubscription.unsubscribe();
|
this.chainTipSubscription.unsubscribe();
|
||||||
|
|||||||
@@ -20,39 +20,46 @@
|
|||||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
<div class="btn-toggle-rows" name="radioBasic">
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
<div class="btn-group btn-group-toggle">
|
||||||
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
||||||
(LIVE)
|
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
|
||||||
</label>
|
(LIVE)
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
</label>
|
||||||
<input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
24H
|
<input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
|
||||||
</label>
|
24H
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
</label>
|
||||||
<input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
</label>
|
<input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
</label>
|
||||||
<input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
</label>
|
<input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
</label>
|
||||||
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
</label>
|
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
</label>
|
||||||
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
|
</div>
|
||||||
</label>
|
<div class="btn-group btn-group-toggle">
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
|
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
|
<input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
|
<input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
|
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</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>
|
||||||
<div class="small-buttons">
|
<div class="small-buttons">
|
||||||
<div ngbDropdown #myDrop="ngbDropdown">
|
<div ngbDropdown #myDrop="ngbDropdown">
|
||||||
|
|||||||
@@ -53,17 +53,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formRadioGroup.mining {
|
.formRadioGroup.mining {
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -100px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formRadioGroup.no-menu {
|
.formRadioGroup.no-menu {
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -33px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,8 +72,10 @@ export class StatisticsComponent implements OnInit {
|
|||||||
this.route
|
this.route
|
||||||
.fragment
|
.fragment
|
||||||
.subscribe((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 });
|
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') {
|
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
|
||||||
return this.apiService.list3YStatistics$();
|
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) => {
|
.subscribe((mempoolStats: any) => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #timeEstimateDefault>
|
<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>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
|
|||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.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 { LiquidUnblinding } from './liquid-ublinding';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { Price, PriceService } from '../../services/price.service';
|
import { Price, PriceService } from '../../services/price.service';
|
||||||
@@ -65,7 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
isCached: boolean = false;
|
isCached: boolean = false;
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
timeAvg$: Observable<number>;
|
da$: Observable<DifficultyAdjustment>;
|
||||||
liquidUnblinding = new LiquidUnblinding();
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
inputIndex: number;
|
inputIndex: number;
|
||||||
outputIndex: number;
|
outputIndex: number;
|
||||||
@@ -117,11 +117,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.setFlowEnabled();
|
this.setFlowEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.timeAvg$ = timer(0, 1000)
|
this.da$ = this.stateService.difficultyAdjustment$.pipe(
|
||||||
.pipe(
|
tap(() => {
|
||||||
switchMap(() => this.stateService.difficultyAdjustment$),
|
this.now = Date.now();
|
||||||
map((da) => da.timeAvg)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||||
this.fragmentParams = new URLSearchParams(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.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
||||||
|
this.now = Date.now();
|
||||||
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
||||||
this.mempoolPosition = txPosition.position;
|
this.mempoolPosition = txPosition.position;
|
||||||
if (this.tx && !this.tx.status.confirmed) {
|
if (this.tx && !this.tx.status.confirmed) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
|
txid: txPosition.txid,
|
||||||
mempoolPosition: this.mempoolPosition
|
mempoolPosition: this.mempoolPosition
|
||||||
});
|
});
|
||||||
this.txInBlockIndex = this.mempoolPosition.block;
|
this.txInBlockIndex = this.mempoolPosition.block;
|
||||||
@@ -359,6 +361,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
|
txid: tx.txid,
|
||||||
txFeePerVSize: tx.effectiveFeePerVsize,
|
txFeePerVSize: tx.effectiveFeePerVsize,
|
||||||
mempoolPosition: this.mempoolPosition,
|
mempoolPosition: this.mempoolPosition,
|
||||||
});
|
});
|
||||||
@@ -434,12 +437,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
||||||
|
this.now = Date.now();
|
||||||
|
|
||||||
if (!this.tx || this.mempoolPosition) {
|
if (!this.tx || this.mempoolPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.now = Date.now();
|
|
||||||
|
|
||||||
const txFeePerVSize =
|
const txFeePerVSize =
|
||||||
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export interface SinglePoolStats {
|
|||||||
logo: string;
|
logo: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
avgMatchRate: number;
|
avgMatchRate: number;
|
||||||
|
avgFeeDelta: number;
|
||||||
}
|
}
|
||||||
export interface PoolsStats {
|
export interface PoolsStats {
|
||||||
blockCount: number;
|
blockCount: number;
|
||||||
@@ -153,6 +154,8 @@ export interface BlockExtended extends Block {
|
|||||||
export interface BlockAudit extends BlockExtended {
|
export interface BlockAudit extends BlockExtended {
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
|
freshTxs: string[],
|
||||||
|
sigopTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees: number,
|
expectedFees: number,
|
||||||
expectedWeight: number,
|
expectedWeight: number,
|
||||||
@@ -169,6 +172,7 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbfTransaction extends TransactionStripped {
|
interface RbfTransaction extends TransactionStripped {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface WebsocketResponse {
|
|||||||
'track-rbf'?: string;
|
'track-rbf'?: string;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'track-bisq-market'?: string;
|
'track-bisq-market'?: string;
|
||||||
|
'refresh-blocks'?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplacedTransaction extends Transaction {
|
export interface ReplacedTransaction extends Transaction {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
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 { Observable } from 'rxjs';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
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');
|
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[]> {
|
getTransactionTimes$(txIds: string[]): Observable<number[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
txIds.forEach((txId: string) => {
|
txIds.forEach((txId: string) => {
|
||||||
@@ -245,9 +249,9 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockAudit$(hash: string) : Observable<any> {
|
getBlockAudit$(hash: string) : Observable<BlockAudit> {
|
||||||
return this.httpClient.get<any>(
|
return this.httpClient.get<BlockAudit>(
|
||||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' }
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
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 { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.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 { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||||
import { Router, NavigationStart } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { map, shareReplay } from 'rxjs/operators';
|
import { map, scan, shareReplay, tap } from 'rxjs/operators';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
interface MarkBlockState {
|
export interface MarkBlockState {
|
||||||
blockHeight?: number;
|
blockHeight?: number;
|
||||||
|
txid?: string;
|
||||||
mempoolBlockIndex?: number;
|
mempoolBlockIndex?: number;
|
||||||
txFeePerVSize?: number;
|
txFeePerVSize?: number;
|
||||||
mempoolPosition?: MempoolPosition;
|
mempoolPosition?: MempoolPosition;
|
||||||
@@ -100,6 +101,7 @@ export class StateService {
|
|||||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||||
|
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||||
txRbfInfo$ = new Subject<RbfTree>();
|
txRbfInfo$ = new Subject<RbfTree>();
|
||||||
rbfLatest$ = 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.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') {
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
this.network = this.env.BASE_MODULE;
|
this.network = this.env.BASE_MODULE;
|
||||||
this.networkChanged$.next(this.env.BASE_MODULE);
|
this.networkChanged$.next(this.env.BASE_MODULE);
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleResponse(response: WebsocketResponse) {
|
handleResponse(response: WebsocketResponse) {
|
||||||
|
let reinitBlocks = false;
|
||||||
|
|
||||||
if (response.blocks && response.blocks.length) {
|
if (response.blocks && response.blocks.length) {
|
||||||
const blocks = response.blocks;
|
const blocks = response.blocks;
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
@@ -256,9 +258,11 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.block) {
|
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.updateChainTip(response.block.height);
|
||||||
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
||||||
|
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
||||||
|
reinitBlocks = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.txConfirmed) {
|
if (response.txConfirmed) {
|
||||||
@@ -369,5 +373,9 @@ export class WebsocketService {
|
|||||||
if (response['git-commit']) {
|
if (response['git-commit']) {
|
||||||
this.stateService.backendInfo$.next(response['git-commit']);
|
this.stateService.backendInfo$.next(response['git-commit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reinitBlocks) {
|
||||||
|
this.websocketSubject.next({'refresh-blocks': true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,15 @@
|
|||||||
<div class="col-sm-12">
|
<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://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="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://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://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>
|
<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>
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ html:lang(ru) .card-title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fee-distribution-chart {
|
.fee-distribution-chart {
|
||||||
height: 250px;
|
height: 265px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fees-wrapper-tooltip-chart {
|
.fees-wrapper-tooltip-chart {
|
||||||
|
|||||||
@@ -361,10 +361,10 @@ BITCOIN_ELECTRS_REPO_NAME=electrs
|
|||||||
BITCOIN_ELECTRS_REPO_BRANCH=mempool
|
BITCOIN_ELECTRS_REPO_BRANCH=mempool
|
||||||
BITCOIN_ELECTRS_LATEST_RELEASE=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_NAME=electrs
|
||||||
ELEMENTS_ELECTRS_REPO_BRANCH=new-index
|
ELEMENTS_ELECTRS_REPO_BRANCH=mempool
|
||||||
ELEMENTS_ELECTRS_LATEST_RELEASE=new-index
|
ELEMENTS_ELECTRS_LATEST_RELEASE=mempool
|
||||||
|
|
||||||
LIQUID_ASSET_REGISTRY_DB_URL=https://github.com/blockstream/asset_registry_db
|
LIQUID_ASSET_REGISTRY_DB_URL=https://github.com/blockstream/asset_registry_db
|
||||||
LIQUID_ASSET_REGISTRY_DB_NAME=asset_registry_db
|
LIQUID_ASSET_REGISTRY_DB_NAME=asset_registry_db
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ do for url in / \
|
|||||||
'/api/v1/statistics/2y' \
|
'/api/v1/statistics/2y' \
|
||||||
'/api/v1/statistics/3y' \
|
'/api/v1/statistics/3y' \
|
||||||
'/api/v1/statistics/4y' \
|
'/api/v1/statistics/4y' \
|
||||||
|
'/api/v1/statistics/all' \
|
||||||
'/api/v1/mining/pools/24h' \
|
'/api/v1/mining/pools/24h' \
|
||||||
'/api/v1/mining/pools/3d' \
|
'/api/v1/mining/pools/3d' \
|
||||||
'/api/v1/mining/pools/1w' \
|
'/api/v1/mining/pools/1w' \
|
||||||
|
|||||||
Reference in New Issue
Block a user