mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 02:11:49 +01:00
Merge branch 'master' into nymkappa/bugfix/node-map
This commit is contained in:
commit
13b52c427c
@ -25,7 +25,9 @@
|
|||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
"ADVANCED_TRANSACTION_SELECTION": false
|
"ADVANCED_GBT_AUDIT": false,
|
||||||
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
|
"TRANSACTION_INDEXING": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
@ -26,7 +26,9 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
||||||
"ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__"
|
"ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
|
||||||
|
"ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
|
||||||
|
"TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__"
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -38,7 +38,9 @@ describe('Mempool Backend Config', () => {
|
|||||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
ADVANCED_TRANSACTION_SELECTION: false,
|
ADVANCED_GBT_AUDIT: false,
|
||||||
|
ADVANCED_GBT_MEMPOOL: false,
|
||||||
|
TRANSACTION_INDEXING: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
|
@ -10,9 +10,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
|
|||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||||
: { censored: string[], added: string[], score: number } {
|
: { censored: string[], added: string[], fresh: string[], score: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], score: 0 };
|
return { censored: [], added: [], fresh: [], score: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
@ -83,7 +83,17 @@ class Audit {
|
|||||||
} else {
|
} else {
|
||||||
if (!isDisplaced[tx.txid]) {
|
if (!isDisplaced[tx.txid]) {
|
||||||
added.push(tx.txid);
|
added.push(tx.txid);
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
|
let blockIndex = -1;
|
||||||
|
let index = -1;
|
||||||
|
projectedBlocks.forEach((block, bi) => {
|
||||||
|
const i = block.transactionIds.indexOf(tx.txid);
|
||||||
|
if (i >= 0) {
|
||||||
|
blockIndex = bi;
|
||||||
|
index = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
overflowWeight += tx.weight;
|
overflowWeight += tx.weight;
|
||||||
}
|
}
|
||||||
totalWeight += tx.weight;
|
totalWeight += tx.weight;
|
||||||
@ -119,48 +129,10 @@ class Audit {
|
|||||||
return {
|
return {
|
||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
|
fresh,
|
||||||
score
|
score
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlockAuditScores(fromHeight?: number, limit: number = 15): Promise<AuditScore[]> {
|
|
||||||
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
|
||||||
const returnScores: AuditScore[] = [];
|
|
||||||
|
|
||||||
if (currentHeight < 0) {
|
|
||||||
return returnScores;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
|
||||||
const block = blocks.getBlocks().find((b) => b.height === currentHeight);
|
|
||||||
if (block?.extras?.matchRate != null) {
|
|
||||||
returnScores.push({
|
|
||||||
hash: block.id,
|
|
||||||
matchRate: block.extras.matchRate
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let currentHash;
|
|
||||||
if (!currentHash && Common.indexingEnabled()) {
|
|
||||||
const dbBlock = await blocksRepository.$getBlockByHeight(currentHeight);
|
|
||||||
if (dbBlock && dbBlock['id']) {
|
|
||||||
currentHash = dbBlock['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!currentHash) {
|
|
||||||
currentHash = await bitcoinApi.$getBlockHash(currentHeight);
|
|
||||||
}
|
|
||||||
if (currentHash) {
|
|
||||||
const auditScore = await blocksAuditsRepository.$getBlockAuditScore(currentHash);
|
|
||||||
returnScores.push({
|
|
||||||
hash: currentHash,
|
|
||||||
matchRate: auditScore?.matchRate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentHeight--;
|
|
||||||
}
|
|
||||||
return returnScores;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Audit();
|
export default new Audit();
|
@ -10,7 +10,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
$getBlockHeader(hash: string): Promise<string>;
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||||
$getRawBlock(hash: string): Promise<string>;
|
$getRawBlock(hash: string): Promise<Buffer>;
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
@ -81,7 +81,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return this.bitcoindClient.getBlock(hash, 0)
|
return this.bitcoindClient.getBlock(hash, 0)
|
||||||
.then((raw: string) => Buffer.from(raw, "hex"));
|
.then((raw: string) => Buffer.from(raw, "hex"));
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,14 @@ import logger from '../../logger';
|
|||||||
import blocks from '../blocks';
|
import blocks from '../blocks';
|
||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
import difficultyAdjustment from '../difficulty-adjustment';
|
import difficultyAdjustment from '../difficulty-adjustment';
|
||||||
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
|
||||||
@ -89,6 +90,7 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||||
;
|
;
|
||||||
|
|
||||||
@ -187,29 +189,36 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
res.status(501).send(`Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = mempool.getMempool()[req.params.txId];
|
const tx = mempool.getMempool()[req.params.txId];
|
||||||
if (!tx) {
|
if (tx) {
|
||||||
res.status(404).send(`Transaction doesn't exist in the mempool.`);
|
if (tx?.cpfpChecked) {
|
||||||
|
res.json({
|
||||||
|
ancestors: tx.ancestors,
|
||||||
|
bestDescendant: tx.bestDescendant || null,
|
||||||
|
descendants: tx.descendants || null,
|
||||||
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
||||||
|
|
||||||
|
res.json(cpfpInfo);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
const cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
|
if (cpfpInfo) {
|
||||||
|
res.json(cpfpInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
res.status(404).send(`Transaction has no CPFP info available.`);
|
||||||
if (tx.cpfpChecked) {
|
|
||||||
res.json({
|
|
||||||
ancestors: tx.ancestors,
|
|
||||||
bestDescendant: tx.bestDescendant || null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
|
|
||||||
|
|
||||||
res.json(cpfpInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackendInfo(req: Request, res: Response) {
|
private getBackendInfo(req: Request, res: Response) {
|
||||||
@ -324,6 +333,16 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getStrippedBlockTransactions(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getBlock(req: Request, res: Response) {
|
private async getBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const block = await blocks.$getBlock(req.params.hash);
|
const block = await blocks.$getBlock(req.params.hash);
|
||||||
@ -356,9 +375,9 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStrippedBlockTransactions(req: Request, res: Response) {
|
private async getBlockAuditSummary(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
|
const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -55,9 +55,9 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
|
||||||
.then((response) => response.data);
|
.then((response) => { return Buffer.from(response.data); });
|
||||||
}
|
}
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||||
|
@ -21,10 +21,13 @@ import fiatConversion from './fiat-conversion';
|
|||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
import { Block } from 'bitcoinjs-lib';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -260,7 +263,7 @@ class Blocks {
|
|||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks summaries for the block txs visualization
|
* [INDEXING] Index all blocks summaries for the block txs visualization
|
||||||
*/
|
*/
|
||||||
public async $generateBlocksSummariesDatabase() {
|
public async $generateBlocksSummariesDatabase(): Promise<void> {
|
||||||
if (Common.blocksSummariesIndexingEnabled() === false) {
|
if (Common.blocksSummariesIndexingEnabled() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -316,6 +319,57 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Index transaction CPFP data for all blocks
|
||||||
|
*/
|
||||||
|
public async $generateCPFPDatabase(): Promise<void> {
|
||||||
|
if (Common.cpfpIndexingEnabled() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all indexed block hash
|
||||||
|
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||||
|
|
||||||
|
if (!unindexedBlocks?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let count = 0;
|
||||||
|
let countThisRun = 0;
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
for (const block of unindexedBlocks) {
|
||||||
|
// Logging
|
||||||
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
|
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
|
||||||
|
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
||||||
|
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
countThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
count++;
|
||||||
|
countThisRun++;
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
@ -359,7 +413,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
++indexedThisRun;
|
++indexedThisRun;
|
||||||
++totalIndexed;
|
++totalIndexed;
|
||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
@ -461,9 +515,13 @@ class Blocks {
|
|||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
|
||||||
|
await cpfpRepository.$deleteClustersFrom(lastBlock['height'] - 10);
|
||||||
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);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
@ -489,6 +547,9 @@ class Blocks {
|
|||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
||||||
|
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,7 +651,7 @@ class Blocks {
|
|||||||
if (skipMemoryCache === false) {
|
if (skipMemoryCache === false) {
|
||||||
// Check the memory cache
|
// Check the memory cache
|
||||||
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
|
||||||
if (cachedSummary) {
|
if (cachedSummary?.transactions?.length) {
|
||||||
return cachedSummary.transactions;
|
return cachedSummary.transactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,7 +659,7 @@ class Blocks {
|
|||||||
// Check if it's indexed in db
|
// Check if it's indexed in db
|
||||||
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
|
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
|
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
|
||||||
if (indexedSummary !== undefined) {
|
if (indexedSummary !== undefined && indexedSummary?.transactions?.length) {
|
||||||
return indexedSummary.transactions;
|
return indexedSummary.transactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -651,6 +712,22 @@ class Blocks {
|
|||||||
return returnBlocks;
|
return returnBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
||||||
|
let summary;
|
||||||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
|
summary = await BlocksAuditsRepository.$getBlockAudit(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
return this.lastDifficultyAdjustmentTime;
|
return this.lastDifficultyAdjustmentTime;
|
||||||
}
|
}
|
||||||
@ -662,6 +739,62 @@ class Blocks {
|
|||||||
public getCurrentBlockHeight(): number {
|
public getCurrentBlockHeight(): number {
|
||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||||
|
let transactions;
|
||||||
|
if (false/*Common.blocksSummariesIndexingEnabled()*/) {
|
||||||
|
transactions = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
const rawBlock = await bitcoinApi.$getRawBlock(hash);
|
||||||
|
const block = Block.fromBuffer(rawBlock);
|
||||||
|
const txMap = {};
|
||||||
|
for (const tx of block.transactions || []) {
|
||||||
|
txMap[tx.getId()] = tx;
|
||||||
|
}
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (txMap[tx.txid]?.ins) {
|
||||||
|
tx.vin = txMap[tx.txid].ins.map(vin => {
|
||||||
|
return {
|
||||||
|
txid: vin.hash
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
|
transactions = block.tx.map(tx => {
|
||||||
|
tx.vsize = tx.weight / 4;
|
||||||
|
return tx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cluster: TransactionStripped[] = [];
|
||||||
|
let ancestors: { [txid: string]: boolean } = {};
|
||||||
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
|
const tx = transactions[i];
|
||||||
|
if (!ancestors[tx.txid]) {
|
||||||
|
let totalFee = 0;
|
||||||
|
let totalVSize = 0;
|
||||||
|
cluster.forEach(tx => {
|
||||||
|
totalFee += tx?.fee || 0;
|
||||||
|
totalVSize += tx.vsize;
|
||||||
|
});
|
||||||
|
const effectiveFeePerVsize = (totalFee * 100_000_000) / totalVSize;
|
||||||
|
if (cluster.length > 1) {
|
||||||
|
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: (tx.fee || 0) * 100_000_000 }; }), effectiveFeePerVsize);
|
||||||
|
for (const tx of cluster) {
|
||||||
|
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cluster = [];
|
||||||
|
ancestors = {};
|
||||||
|
}
|
||||||
|
cluster.push(tx);
|
||||||
|
tx.vin.forEach(vin => {
|
||||||
|
ancestors[vin.txid] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await blocksRepository.$setCPFPIndexed(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Blocks();
|
export default new Blocks();
|
||||||
|
@ -187,6 +187,13 @@ export class Common {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static cpfpIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.TRANSACTION_INDEXING === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static setDateMidnight(date: Date): void {
|
static setDateMidnight(date: Date): void {
|
||||||
date.setUTCHours(0);
|
date.setUTCHours(0);
|
||||||
date.setUTCMinutes(0);
|
date.setUTCMinutes(0);
|
||||||
|
@ -4,8 +4,8 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 44;
|
private static currentVersion = 49;
|
||||||
private queryTimeout = 900_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
|
|
||||||
@ -107,18 +107,22 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
|
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
|
||||||
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
|
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
|
||||||
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
|
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
|
||||||
|
await this.updateToSchemaVersion(2);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 3) {
|
if (databaseSchemaVersion < 3) {
|
||||||
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
|
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
|
||||||
|
await this.updateToSchemaVersion(3);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 4) {
|
if (databaseSchemaVersion < 4) {
|
||||||
await this.$executeQuery('DROP table IF EXISTS blocks;');
|
await this.$executeQuery('DROP table IF EXISTS blocks;');
|
||||||
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
||||||
|
await this.updateToSchemaVersion(4);
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
||||||
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
||||||
@ -141,11 +145,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
||||||
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
||||||
|
await this.updateToSchemaVersion(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 7 && isBitcoin === true) {
|
if (databaseSchemaVersion < 7 && isBitcoin === true) {
|
||||||
await this.$executeQuery('DROP table IF EXISTS hashrates;');
|
await this.$executeQuery('DROP table IF EXISTS hashrates;');
|
||||||
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
||||||
|
await this.updateToSchemaVersion(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
||||||
@ -155,6 +161,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
||||||
|
await this.updateToSchemaVersion(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
||||||
@ -162,10 +169,12 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
||||||
|
await this.updateToSchemaVersion(9);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 10 && isBitcoin === true) {
|
if (databaseSchemaVersion < 10 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
|
||||||
|
await this.updateToSchemaVersion(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
||||||
@ -178,11 +187,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(11);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 12 && isBitcoin === true) {
|
if (databaseSchemaVersion < 12 && isBitcoin === true) {
|
||||||
// No need to re-index because the new data type can contain larger values
|
// No need to re-index because the new data type can contain larger values
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 13 && isBitcoin === true) {
|
if (databaseSchemaVersion < 13 && isBitcoin === true) {
|
||||||
@ -190,6 +201,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
if (databaseSchemaVersion < 14 && isBitcoin === true) {
|
||||||
@ -197,37 +209,45 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
||||||
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(14);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
if (databaseSchemaVersion < 16 && isBitcoin === true) {
|
||||||
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
|
||||||
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
|
||||||
|
await this.updateToSchemaVersion(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 17 && isBitcoin === true) {
|
if (databaseSchemaVersion < 17 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
|
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
|
||||||
|
await this.updateToSchemaVersion(17);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 18 && isBitcoin === true) {
|
if (databaseSchemaVersion < 18 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
|
||||||
|
await this.updateToSchemaVersion(18);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 19) {
|
if (databaseSchemaVersion < 19) {
|
||||||
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
|
||||||
|
await this.updateToSchemaVersion(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 20 && isBitcoin === true) {
|
if (databaseSchemaVersion < 20 && isBitcoin === true) {
|
||||||
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
||||||
|
await this.updateToSchemaVersion(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 21) {
|
if (databaseSchemaVersion < 21) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
|
||||||
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
|
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
|
||||||
|
await this.updateToSchemaVersion(21);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 22 && isBitcoin === true) {
|
if (databaseSchemaVersion < 22 && isBitcoin === true) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
||||||
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
||||||
|
await this.updateToSchemaVersion(22);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 23) {
|
if (databaseSchemaVersion < 23) {
|
||||||
@ -240,11 +260,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(23);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 24 && isBitcoin == true) {
|
if (databaseSchemaVersion < 24 && isBitcoin == true) {
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
||||||
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
||||||
|
await this.updateToSchemaVersion(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
||||||
@ -252,6 +274,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
||||||
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
||||||
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
||||||
|
await this.updateToSchemaVersion(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 26 && isBitcoin === true) {
|
if (databaseSchemaVersion < 26 && isBitcoin === true) {
|
||||||
@ -262,6 +285,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(26);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 27 && isBitcoin === true) {
|
if (databaseSchemaVersion < 27 && isBitcoin === true) {
|
||||||
@ -271,6 +295,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(27);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 28 && isBitcoin === true) {
|
if (databaseSchemaVersion < 28 && isBitcoin === true) {
|
||||||
@ -280,6 +305,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(`TRUNCATE lightning_stats`);
|
await this.$executeQuery(`TRUNCATE lightning_stats`);
|
||||||
await this.$executeQuery(`TRUNCATE node_stats`);
|
await this.$executeQuery(`TRUNCATE node_stats`);
|
||||||
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
|
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
|
||||||
|
await this.updateToSchemaVersion(28);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 29 && isBitcoin === true) {
|
if (databaseSchemaVersion < 29 && isBitcoin === true) {
|
||||||
@ -291,41 +317,50 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
|
||||||
|
await this.updateToSchemaVersion(29);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 30 && isBitcoin === true) {
|
if (databaseSchemaVersion < 30 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
|
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
|
||||||
|
await this.updateToSchemaVersion(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
|
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
|
||||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
|
||||||
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
|
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
|
||||||
|
await this.updateToSchemaVersion(31);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 32 && isBitcoin == true) {
|
if (databaseSchemaVersion < 32 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
|
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
||||||
|
await this.updateToSchemaVersion(33);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 34 && isBitcoin == true) {
|
if (databaseSchemaVersion < 34 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
|
await this.updateToSchemaVersion(34);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 35 && isBitcoin == true) {
|
if (databaseSchemaVersion < 35 && isBitcoin == true) {
|
||||||
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
||||||
|
await this.updateToSchemaVersion(35);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 36 && isBitcoin == true) {
|
if (databaseSchemaVersion < 36 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
||||||
|
await this.updateToSchemaVersion(36);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 37 && isBitcoin == true) {
|
if (databaseSchemaVersion < 37 && isBitcoin == true) {
|
||||||
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
|
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
|
||||||
|
await this.updateToSchemaVersion(37);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 38 && isBitcoin == true) {
|
if (databaseSchemaVersion < 38 && isBitcoin == true) {
|
||||||
@ -336,34 +371,76 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(`TRUNCATE node_stats`);
|
await this.$executeQuery(`TRUNCATE node_stats`);
|
||||||
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
|
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
|
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
|
||||||
|
await this.updateToSchemaVersion(38);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 39 && isBitcoin === true) {
|
if (databaseSchemaVersion < 39 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
||||||
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
||||||
|
await this.updateToSchemaVersion(39);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 40 && isBitcoin === true) {
|
if (databaseSchemaVersion < 40 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
||||||
|
await this.updateToSchemaVersion(40);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 41 && isBitcoin === true) {
|
if (databaseSchemaVersion < 41 && isBitcoin === true) {
|
||||||
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
|
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
|
||||||
|
await this.updateToSchemaVersion(41);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
||||||
|
await this.updateToSchemaVersion(42);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 43 && isBitcoin === true) {
|
if (databaseSchemaVersion < 43 && isBitcoin === true) {
|
||||||
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
||||||
|
await this.updateToSchemaVersion(43);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 44 && isBitcoin === true) {
|
if (databaseSchemaVersion < 44 && isBitcoin === true) {
|
||||||
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
|
||||||
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
|
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
|
||||||
|
await this.updateToSchemaVersion(44);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 45 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(45);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 46) {
|
||||||
|
await this.$executeQuery(`ALTER TABLE blocks MODIFY blockTimestamp timestamp NOT NULL DEFAULT 0`);
|
||||||
|
await this.updateToSchemaVersion(46);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 47) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` ADD cpfp_indexed tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters'));
|
||||||
|
await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions'));
|
||||||
|
await this.updateToSchemaVersion(47);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 48 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
|
||||||
|
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(48);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 49 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
||||||
|
await this.updateToSchemaVersion(49);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,6 +579,10 @@ class DatabaseMigration {
|
|||||||
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
|
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateToSchemaVersion(version): Promise<void> {
|
||||||
|
await this.$executeQuery(`UPDATE state SET number = ${version} WHERE name = 'schema_version';`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print current database version
|
* Print current database version
|
||||||
*/
|
*/
|
||||||
@ -813,6 +894,25 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateCPFPTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS cpfp_clusters (
|
||||||
|
root varchar(65) NOT NULL,
|
||||||
|
height int(10) NOT NULL,
|
||||||
|
txs JSON DEFAULT NULL,
|
||||||
|
fee_rate double unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (root)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateTransactionsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
txid varchar(65) NOT NULL,
|
||||||
|
cluster varchar(65) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (txid),
|
||||||
|
FOREIGN KEY (cluster) REFERENCES cpfp_clusters (root) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
|
@ -128,6 +128,21 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getChannelsWithoutSourceChecked(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.source_checked != 1
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
|
public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM channels WHERE created IS NULL`;
|
const query = `SELECT * FROM channels WHERE created IS NULL`;
|
||||||
@ -257,6 +272,108 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getChannelByClosingId(transactionId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.closing_transaction_id = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [transactionId]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
rows[0].outputs = JSON.parse(rows[0].outputs);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelByClosingId error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getChannelsByOpeningId(transactionId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
channels.*
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.transaction_id = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [transactionId]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows.map(row => {
|
||||||
|
row.outputs = JSON.parse(row.outputs);
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelsByOpeningId error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateClosingInfo(channelInfo: { id: string, node1_closing_balance: number, node2_closing_balance: number, closed_by: string | null, closing_fee: number, outputs: ILightningApi.ForensicOutput[]}): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels SET
|
||||||
|
node1_closing_balance = ?,
|
||||||
|
node2_closing_balance = ?,
|
||||||
|
closed_by = ?,
|
||||||
|
closing_fee = ?,
|
||||||
|
outputs = ?
|
||||||
|
WHERE channels.id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [
|
||||||
|
channelInfo.node1_closing_balance || 0,
|
||||||
|
channelInfo.node2_closing_balance || 0,
|
||||||
|
channelInfo.closed_by,
|
||||||
|
channelInfo.closing_fee || 0,
|
||||||
|
JSON.stringify(channelInfo.outputs),
|
||||||
|
channelInfo.id,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$updateClosingInfo error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateOpeningInfo(channelInfo: { id: string, node1_funding_balance: number, node2_funding_balance: number, funding_ratio: number, single_funded: boolean | void }): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels SET
|
||||||
|
node1_funding_balance = ?,
|
||||||
|
node2_funding_balance = ?,
|
||||||
|
funding_ratio = ?,
|
||||||
|
single_funded = ?
|
||||||
|
WHERE channels.id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [
|
||||||
|
channelInfo.node1_funding_balance || 0,
|
||||||
|
channelInfo.node2_funding_balance || 0,
|
||||||
|
channelInfo.funding_ratio,
|
||||||
|
channelInfo.single_funded ? 1 : 0,
|
||||||
|
channelInfo.id,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$updateOpeningInfo error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $markChannelSourceChecked(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
UPDATE channels
|
||||||
|
SET source_checked = 1
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
await DB.query<ResultSetHeader>(query, [id]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$markChannelSourceChecked error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
let channelStatusFilter;
|
let channelStatusFilter;
|
||||||
@ -385,11 +502,15 @@ class ChannelsApi {
|
|||||||
'transaction_id': channel.transaction_id,
|
'transaction_id': channel.transaction_id,
|
||||||
'transaction_vout': channel.transaction_vout,
|
'transaction_vout': channel.transaction_vout,
|
||||||
'closing_transaction_id': channel.closing_transaction_id,
|
'closing_transaction_id': channel.closing_transaction_id,
|
||||||
|
'closing_fee': channel.closing_fee,
|
||||||
'closing_reason': channel.closing_reason,
|
'closing_reason': channel.closing_reason,
|
||||||
'closing_date': channel.closing_date,
|
'closing_date': channel.closing_date,
|
||||||
'updated_at': channel.updated_at,
|
'updated_at': channel.updated_at,
|
||||||
'created': channel.created,
|
'created': channel.created,
|
||||||
'status': channel.status,
|
'status': channel.status,
|
||||||
|
'funding_ratio': channel.funding_ratio,
|
||||||
|
'closed_by': channel.closed_by,
|
||||||
|
'single_funded': !!channel.single_funded,
|
||||||
'node_left': {
|
'node_left': {
|
||||||
'alias': channel.alias_left,
|
'alias': channel.alias_left,
|
||||||
'public_key': channel.node1_public_key,
|
'public_key': channel.node1_public_key,
|
||||||
@ -404,6 +525,9 @@ class ChannelsApi {
|
|||||||
'updated_at': channel.node1_updated_at,
|
'updated_at': channel.node1_updated_at,
|
||||||
'longitude': channel.node1_longitude,
|
'longitude': channel.node1_longitude,
|
||||||
'latitude': channel.node1_latitude,
|
'latitude': channel.node1_latitude,
|
||||||
|
'funding_balance': channel.node1_funding_balance,
|
||||||
|
'closing_balance': channel.node1_closing_balance,
|
||||||
|
'initiated_close': channel.closed_by === channel.node1_public_key ? true : undefined,
|
||||||
},
|
},
|
||||||
'node_right': {
|
'node_right': {
|
||||||
'alias': channel.alias_right,
|
'alias': channel.alias_right,
|
||||||
@ -419,6 +543,9 @@ class ChannelsApi {
|
|||||||
'updated_at': channel.node2_updated_at,
|
'updated_at': channel.node2_updated_at,
|
||||||
'longitude': channel.node2_longitude,
|
'longitude': channel.node2_longitude,
|
||||||
'latitude': channel.node2_latitude,
|
'latitude': channel.node2_latitude,
|
||||||
|
'funding_balance': channel.node2_funding_balance,
|
||||||
|
'closing_balance': channel.node2_closing_balance,
|
||||||
|
'initiated_close': channel.closed_by === channel.node2_public_key ? true : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -83,4 +83,10 @@ export namespace ILightningApi {
|
|||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
is_known: boolean;
|
is_known: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForensicOutput {
|
||||||
|
node?: 1 | 2;
|
||||||
|
type: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
}
|
}
|
@ -155,6 +155,7 @@ class MempoolBlocks {
|
|||||||
if (newMempool[txid] && mempool[txid]) {
|
if (newMempool[txid] && mempool[txid]) {
|
||||||
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
|
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
|
||||||
newMempool[txid].ancestors = mempool[txid].ancestors;
|
newMempool[txid].ancestors = mempool[txid].ancestors;
|
||||||
|
newMempool[txid].descendants = mempool[txid].descendants;
|
||||||
newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
|
newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
|
||||||
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
|
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
|
||||||
}
|
}
|
||||||
|
@ -283,9 +283,12 @@ class MiningRoutes {
|
|||||||
|
|
||||||
private async $getBlockAuditScores(req: Request, res: Response) {
|
private async $getBlockAuditScores(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
let height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||||
|
if (height == null) {
|
||||||
|
height = await BlocksRepository.$mostRecentBlockHeight();
|
||||||
|
}
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await audits.$getBlockAuditScores(height, 15));
|
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
@ -108,36 +108,38 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }:
|
|||||||
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
||||||
blockWeight += nextTx.ancestorWeight;
|
blockWeight += nextTx.ancestorWeight;
|
||||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
|
const descendants: AuditTransaction[] = [];
|
||||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||||
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
|
||||||
sortedTxSet.forEach((ancestor, i, arr) => {
|
|
||||||
|
while (sortedTxSet.length) {
|
||||||
|
const ancestor = sortedTxSet.pop();
|
||||||
const mempoolTx = mempool[ancestor.txid];
|
const mempoolTx = mempool[ancestor.txid];
|
||||||
if (ancestor && !ancestor?.used) {
|
if (ancestor && !ancestor?.used) {
|
||||||
ancestor.used = true;
|
ancestor.used = true;
|
||||||
// update original copy of this tx with effective fee rate & relatives data
|
// update original copy of this tx with effective fee rate & relatives data
|
||||||
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
|
||||||
mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => {
|
mempoolTx.ancestors = sortedTxSet.map((a) => {
|
||||||
|
return {
|
||||||
|
txid: a.txid,
|
||||||
|
fee: a.fee,
|
||||||
|
weight: a.weight,
|
||||||
|
};
|
||||||
|
}).reverse();
|
||||||
|
mempoolTx.descendants = descendants.map((a) => {
|
||||||
return {
|
return {
|
||||||
txid: a.txid,
|
txid: a.txid,
|
||||||
fee: a.fee,
|
fee: a.fee,
|
||||||
weight: a.weight,
|
weight: a.weight,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
descendants.push(ancestor);
|
||||||
mempoolTx.cpfpChecked = true;
|
mempoolTx.cpfpChecked = true;
|
||||||
if (i < arr.length - 1) {
|
|
||||||
mempoolTx.bestDescendant = {
|
|
||||||
txid: arr[arr.length - 1].txid,
|
|
||||||
fee: arr[arr.length - 1].fee,
|
|
||||||
weight: arr[arr.length - 1].weight,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
mempoolTx.bestDescendant = null;
|
|
||||||
}
|
|
||||||
transactions.push(ancestor);
|
transactions.push(ancestor);
|
||||||
blockSize += ancestor.size;
|
blockSize += ancestor.size;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||||
if (sortedTxSet.length) {
|
if (sortedTxSet.length) {
|
||||||
|
@ -250,12 +250,12 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true);
|
await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
@ -417,9 +417,8 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
let matchRate;
|
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
|
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||||
await mempoolBlocks.makeBlockTemplates(_memPool, 2);
|
await mempoolBlocks.makeBlockTemplates(_memPool, 2);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||||
@ -428,8 +427,8 @@ class WebsocketHandler {
|
|||||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||||
const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
|
|
||||||
const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
|
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
|
||||||
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.map((tx) => {
|
||||||
return {
|
return {
|
||||||
@ -454,6 +453,7 @@ class WebsocketHandler {
|
|||||||
hash: block.id,
|
hash: block.id,
|
||||||
addedTxs: added,
|
addedTxs: added,
|
||||||
missingTxs: censored,
|
missingTxs: censored,
|
||||||
|
freshTxs: fresh,
|
||||||
matchRate: matchRate,
|
matchRate: matchRate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -467,7 +467,7 @@ class WebsocketHandler {
|
|||||||
delete _memPool[txId];
|
delete _memPool[txId];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
await mempoolBlocks.makeBlockTemplates(_memPool, 2);
|
await mempoolBlocks.makeBlockTemplates(_memPool, 2);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||||
|
@ -29,7 +29,9 @@ interface IConfig {
|
|||||||
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
ADVANCED_TRANSACTION_SELECTION: boolean;
|
ADVANCED_GBT_AUDIT: boolean;
|
||||||
|
ADVANCED_GBT_MEMPOOL: boolean;
|
||||||
|
TRANSACTION_INDEXING: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@ -147,7 +149,9 @@ const defaults: IConfig = {
|
|||||||
'AUTOMATIC_BLOCK_REINDEXING': false,
|
'AUTOMATIC_BLOCK_REINDEXING': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
'ADVANCED_TRANSACTION_SELECTION': false,
|
'ADVANCED_GBT_AUDIT': false,
|
||||||
|
'ADVANCED_GBT_MEMPOOL': false,
|
||||||
|
'TRANSACTION_INDEXING': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
@ -77,6 +77,7 @@ class Indexer {
|
|||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
await mining.$generatePoolHashrateHistory();
|
await mining.$generatePoolHashrateHistory();
|
||||||
await blocks.$generateBlocksSummariesDatabase();
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
|
await blocks.$generateCPFPDatabase();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
@ -28,6 +28,7 @@ export interface BlockAudit {
|
|||||||
height: number,
|
height: number,
|
||||||
hash: string,
|
hash: string,
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
|
freshTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
}
|
}
|
||||||
@ -71,6 +72,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
firstSeen?: number;
|
firstSeen?: number;
|
||||||
effectiveFeePerVsize: number;
|
effectiveFeePerVsize: number;
|
||||||
ancestors?: Ancestor[];
|
ancestors?: Ancestor[];
|
||||||
|
descendants?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
cpfpChecked?: boolean;
|
cpfpChecked?: boolean;
|
||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
@ -118,7 +120,9 @@ interface BestDescendant {
|
|||||||
|
|
||||||
export interface CpfpInfo {
|
export interface CpfpInfo {
|
||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
bestDescendant: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
|
descendants?: Ancestor[];
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionStripped {
|
export interface TransactionStripped {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import blocks from '../api/blocks';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||||
@ -5,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
|||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
|
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), audit.matchRate]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||||
@ -51,7 +52,7 @@ 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, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate
|
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
|
||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||||
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||||
@ -61,11 +62,15 @@ class BlocksAuditRepositories {
|
|||||||
if (rows.length) {
|
if (rows.length) {
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
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].transactions = JSON.parse(rows[0].transactions);
|
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 null;
|
||||||
return rows[0];
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
@ -85,6 +90,20 @@ class BlocksAuditRepositories {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any[] = await DB.query(
|
||||||
|
`SELECT hash, match_rate as matchRate
|
||||||
|
FROM blocks_audits
|
||||||
|
WHERE blocks_audits.height BETWEEN ? AND ?
|
||||||
|
`, [minHeight, maxHeight]);
|
||||||
|
return rows;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksAuditRepositories();
|
export default new BlocksAuditRepositories();
|
||||||
|
@ -662,6 +662,23 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of blocks that have not had CPFP data indexed
|
||||||
|
*/
|
||||||
|
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $setCPFPIndexed(hash: string): Promise<void> {
|
||||||
|
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the oldest block from a consecutive chain of block from the most recent one
|
* Return the oldest block from a consecutive chain of block from the most recent one
|
||||||
*/
|
*/
|
||||||
|
43
backend/src/repositories/CpfpRepository.ts
Normal file
43
backend/src/repositories/CpfpRepository.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
class CpfpRepository {
|
||||||
|
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const txsJson = JSON.stringify(txs);
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
|
||||||
|
VALUE (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
height = ?,
|
||||||
|
txs = ?,
|
||||||
|
fee_rate = ?
|
||||||
|
`,
|
||||||
|
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE from cpfp_clusters
|
||||||
|
WHERE height >= ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CpfpRepository();
|
77
backend/src/repositories/TransactionRepository.ts
Normal file
77
backend/src/repositories/TransactionRepository.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
interface CpfpSummary {
|
||||||
|
txid: string;
|
||||||
|
cluster: string;
|
||||||
|
root: string;
|
||||||
|
txs: Ancestor[];
|
||||||
|
height: number;
|
||||||
|
fee_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionRepository {
|
||||||
|
public async $setCluster(txid: string, cluster: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO transactions
|
||||||
|
(
|
||||||
|
txid,
|
||||||
|
cluster
|
||||||
|
)
|
||||||
|
VALUE (?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
cluster = ?
|
||||||
|
;`,
|
||||||
|
[txid, cluster, cluster]
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
||||||
|
try {
|
||||||
|
let query = `
|
||||||
|
SELECT *
|
||||||
|
FROM transactions
|
||||||
|
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
|
||||||
|
WHERE transactions.txid = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [txid]);
|
||||||
|
if (rows.length) {
|
||||||
|
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
|
||||||
|
return this.convertCpfp(rows[0]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
let matched = false;
|
||||||
|
for (const tx of cpfp.txs) {
|
||||||
|
if (tx.txid === cpfp.txid) {
|
||||||
|
matched = true;
|
||||||
|
} else if (!matched) {
|
||||||
|
descendants.push(tx);
|
||||||
|
} else {
|
||||||
|
ancestors.push(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
descendants,
|
||||||
|
ancestors,
|
||||||
|
effectiveFeePerVsize: cpfp.fee_rate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TransactionRepository();
|
||||||
|
|
@ -5,13 +5,16 @@ import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
|
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||||
|
|
||||||
const throttleDelay = 20; //ms
|
const throttleDelay = 20; //ms
|
||||||
|
const tempCacheSize = 10000;
|
||||||
|
|
||||||
class ForensicsService {
|
class ForensicsService {
|
||||||
loggerTimer = 0;
|
loggerTimer = 0;
|
||||||
closedChannelsScanBlock = 0;
|
closedChannelsScanBlock = 0;
|
||||||
txCache: { [txid: string]: IEsploraApi.Transaction } = {};
|
txCache: { [txid: string]: IEsploraApi.Transaction } = {};
|
||||||
|
tempCached: string[] = [];
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@ -29,6 +32,7 @@ class ForensicsService {
|
|||||||
|
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
await this.$runClosedChannelsForensics(false);
|
await this.$runClosedChannelsForensics(false);
|
||||||
|
await this.$runOpenedChannelsForensics();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -95,16 +99,9 @@ class ForensicsService {
|
|||||||
const lightningScriptReasons: number[] = [];
|
const lightningScriptReasons: number[] = [];
|
||||||
for (const outspend of outspends) {
|
for (const outspend of outspends) {
|
||||||
if (outspend.spent && outspend.txid) {
|
if (outspend.spent && outspend.txid) {
|
||||||
let spendingTx: IEsploraApi.Transaction | undefined = this.txCache[outspend.txid];
|
let spendingTx = await this.fetchTransaction(outspend.txid);
|
||||||
if (!spendingTx) {
|
if (!spendingTx) {
|
||||||
try {
|
continue;
|
||||||
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
|
||||||
await Common.sleep$(throttleDelay);
|
|
||||||
this.txCache[outspend.txid] = spendingTx;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cached.push(spendingTx.txid);
|
cached.push(spendingTx.txid);
|
||||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||||
@ -124,16 +121,9 @@ class ForensicsService {
|
|||||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||||
*/
|
*/
|
||||||
let closingTx: IEsploraApi.Transaction | undefined = this.txCache[channel.closing_transaction_id];
|
let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
|
||||||
if (!closingTx) {
|
if (!closingTx) {
|
||||||
try {
|
continue;
|
||||||
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
|
|
||||||
await Common.sleep$(throttleDelay);
|
|
||||||
this.txCache[channel.closing_transaction_id] = closingTx;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cached.push(closingTx.txid);
|
cached.push(closingTx.txid);
|
||||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||||
@ -174,7 +164,7 @@ class ForensicsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findLightningScript(vin: IEsploraApi.Vin): number {
|
private findLightningScript(vin: IEsploraApi.Vin): number {
|
||||||
const topElement = vin.witness[vin.witness.length - 2];
|
const topElement = vin.witness?.length > 2 ? vin.witness[vin.witness.length - 2] : null;
|
||||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||||
if (topElement === '01') {
|
if (topElement === '01') {
|
||||||
@ -193,7 +183,7 @@ class ForensicsService {
|
|||||||
) {
|
) {
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||||
if (topElement.length === 66) {
|
if (topElement?.length === 66) {
|
||||||
// top element is a public key
|
// top element is a public key
|
||||||
// 'Revoked Lightning HTLC'; Penalty force closed
|
// 'Revoked Lightning HTLC'; Penalty force closed
|
||||||
return 4;
|
return 4;
|
||||||
@ -220,6 +210,249 @@ class ForensicsService {
|
|||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a channel open tx spends funds from a another channel transaction,
|
||||||
|
// we can attribute that output to a specific counterparty
|
||||||
|
private async $runOpenedChannelsForensics(): Promise<void> {
|
||||||
|
const runTimer = Date.now();
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Started running open channel forensics...`);
|
||||||
|
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
|
||||||
|
|
||||||
|
for (const openChannel of channels) {
|
||||||
|
let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
|
||||||
|
if (!openTx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const input of openTx.vin) {
|
||||||
|
const closeChannel = await channelsApi.$getChannelByClosingId(input.txid);
|
||||||
|
if (closeChannel) {
|
||||||
|
// this input directly spends a channel close output
|
||||||
|
await this.$attributeChannelBalances(closeChannel, openChannel, input);
|
||||||
|
} else {
|
||||||
|
const prevOpenChannels = await channelsApi.$getChannelsByOpeningId(input.txid);
|
||||||
|
if (prevOpenChannels?.length) {
|
||||||
|
// this input spends a channel open change output
|
||||||
|
for (const prevOpenChannel of prevOpenChannels) {
|
||||||
|
await this.$attributeChannelBalances(prevOpenChannel, openChannel, input, null, null, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// check if this input spends any swept channel close outputs
|
||||||
|
await this.$attributeSweptChannelCloses(openChannel, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// calculate how much of the total input value is attributable to the channel open output
|
||||||
|
openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee);
|
||||||
|
// save changes to the opening channel, and mark it as checked
|
||||||
|
if (openTx?.vin?.length === 1) {
|
||||||
|
openChannel.single_funded = true;
|
||||||
|
}
|
||||||
|
if (openChannel.node1_funding_balance || openChannel.node2_funding_balance || openChannel.node1_closing_balance || openChannel.node2_closing_balance || openChannel.closed_by) {
|
||||||
|
await channelsApi.$updateOpeningInfo(openChannel);
|
||||||
|
}
|
||||||
|
await channelsApi.$markChannelSourceChecked(openChannel.id);
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
this.truncateTempCache();
|
||||||
|
}
|
||||||
|
if (Date.now() - runTimer > (config.LIGHTNING.FORENSICS_INTERVAL * 1000)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Open channels forensics scan complete.`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
} finally {
|
||||||
|
this.clearTempCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a channel open tx input spends the result of a swept channel close output
|
||||||
|
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
|
||||||
|
let sweepTx = await this.fetchTransaction(input.txid, true);
|
||||||
|
if (!sweepTx) {
|
||||||
|
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const openContribution = sweepTx.vout[input.vout].value;
|
||||||
|
for (const sweepInput of sweepTx.vin) {
|
||||||
|
const lnScriptType = this.findLightningScript(sweepInput);
|
||||||
|
if (lnScriptType > 1) {
|
||||||
|
const closeChannel = await channelsApi.$getChannelByClosingId(sweepInput.txid);
|
||||||
|
if (closeChannel) {
|
||||||
|
const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null);
|
||||||
|
await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $attributeChannelBalances(
|
||||||
|
prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null,
|
||||||
|
initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
// figure out which node controls the input/output
|
||||||
|
let openSide;
|
||||||
|
let prevLocal;
|
||||||
|
let prevRemote;
|
||||||
|
let matched = false;
|
||||||
|
let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
|
||||||
|
if (openChannel.node1_public_key === prevChannel.node1_public_key) {
|
||||||
|
openSide = 1;
|
||||||
|
prevLocal = 1;
|
||||||
|
prevRemote = 2;
|
||||||
|
matched = true;
|
||||||
|
} else if (openChannel.node1_public_key === prevChannel.node2_public_key) {
|
||||||
|
openSide = 1;
|
||||||
|
prevLocal = 2;
|
||||||
|
prevRemote = 1;
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
if (openChannel.node2_public_key === prevChannel.node1_public_key) {
|
||||||
|
openSide = 2;
|
||||||
|
prevLocal = 1;
|
||||||
|
prevRemote = 2;
|
||||||
|
if (matched) {
|
||||||
|
ambiguous = true;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
} else if (openChannel.node2_public_key === prevChannel.node2_public_key) {
|
||||||
|
openSide = 2;
|
||||||
|
prevLocal = 2;
|
||||||
|
prevRemote = 1;
|
||||||
|
if (matched) {
|
||||||
|
ambiguous = true;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched && !ambiguous) {
|
||||||
|
// fetch closing channel transaction and perform forensics on the outputs
|
||||||
|
let prevChannelTx = await this.fetchTransaction(input.txid, true);
|
||||||
|
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||||
|
try {
|
||||||
|
outspends = await bitcoinApi.$getOutspends(input.txid);
|
||||||
|
await Common.sleep$(throttleDelay);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + input.txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
if (!outspends || !prevChannelTx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!linkedOpenings) {
|
||||||
|
if (!prevChannel.outputs || !prevChannel.outputs.length) {
|
||||||
|
prevChannel.outputs = prevChannelTx.vout.map(vout => {
|
||||||
|
return {
|
||||||
|
type: 0,
|
||||||
|
value: vout.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < outspends?.length; i++) {
|
||||||
|
const outspend = outspends[i];
|
||||||
|
const output = prevChannel.outputs[i];
|
||||||
|
if (outspend.spent && outspend.txid) {
|
||||||
|
try {
|
||||||
|
const spendingTx = await this.fetchTransaction(outspend.txid, true);
|
||||||
|
if (spendingTx) {
|
||||||
|
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.type = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attribute outputs to each counterparty, and sum up total known balances
|
||||||
|
prevChannel.outputs[input.vout].node = prevLocal;
|
||||||
|
const isPenalty = prevChannel.outputs.filter((out) => out.type === 2 || out.type === 4)?.length > 0;
|
||||||
|
const normalOutput = [1,3].includes(prevChannel.outputs[input.vout].type);
|
||||||
|
const mutualClose = ((prevChannel.status === 2 || prevChannel.status === 'closed') && prevChannel.closing_reason === 1);
|
||||||
|
let localClosingBalance = 0;
|
||||||
|
let remoteClosingBalance = 0;
|
||||||
|
for (const output of prevChannel.outputs) {
|
||||||
|
if (isPenalty) {
|
||||||
|
// penalty close, so local node takes everything
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else if (output.node) {
|
||||||
|
// this output determinstically linked to one of the counterparties
|
||||||
|
if (output.node === prevLocal) {
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else {
|
||||||
|
remoteClosingBalance += output.value;
|
||||||
|
}
|
||||||
|
} else if (normalOutput && (output.type === 1 || output.type === 3 || (mutualClose && prevChannel.outputs.length === 2))) {
|
||||||
|
// local node had one main output, therefore remote node takes the other
|
||||||
|
remoteClosingBalance += output.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevChannel[`node${prevLocal}_closing_balance`] = localClosingBalance;
|
||||||
|
prevChannel[`node${prevRemote}_closing_balance`] = remoteClosingBalance;
|
||||||
|
prevChannel.closing_fee = prevChannelTx.fee;
|
||||||
|
|
||||||
|
if (initiator && !linkedOpenings) {
|
||||||
|
const initiatorSide = initiator === 'remote' ? prevRemote : prevLocal;
|
||||||
|
prevChannel.closed_by = prevChannel[`node${initiatorSide}_public_key`];
|
||||||
|
}
|
||||||
|
|
||||||
|
// save changes to the closing channel
|
||||||
|
await channelsApi.$updateClosingInfo(prevChannel);
|
||||||
|
} else {
|
||||||
|
if (prevChannelTx.vin.length <= 1) {
|
||||||
|
prevChannel[`node${prevLocal}_funding_balance`] = prevChannel.capacity;
|
||||||
|
prevChannel.single_funded = true;
|
||||||
|
prevChannel.funding_ratio = 1;
|
||||||
|
// save changes to the closing channel
|
||||||
|
await channelsApi.$updateOpeningInfo(prevChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || prevChannelTx?.vout[input.vout]?.value || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTransaction(txid: string, temp: boolean = false): Promise<IEsploraApi.Transaction | null> {
|
||||||
|
let tx = this.txCache[txid];
|
||||||
|
if (!tx) {
|
||||||
|
try {
|
||||||
|
tx = await bitcoinApi.$getRawTransaction(txid);
|
||||||
|
this.txCache[txid] = tx;
|
||||||
|
if (temp) {
|
||||||
|
this.tempCached.push(txid);
|
||||||
|
}
|
||||||
|
await Common.sleep$(throttleDelay);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTempCache(): void {
|
||||||
|
for (const txid of this.tempCached) {
|
||||||
|
delete this.txCache[txid];
|
||||||
|
}
|
||||||
|
this.tempCached = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateTempCache(): void {
|
||||||
|
if (this.tempCached.length > tempCacheSize) {
|
||||||
|
const removed = this.tempCached.splice(0, this.tempCached.length - tempCacheSize);
|
||||||
|
for (const txid of removed) {
|
||||||
|
delete this.txCache[txid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ForensicsService();
|
export default new ForensicsService();
|
||||||
|
@ -31,6 +31,7 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async $runTasks(): Promise<void> {
|
private async $runTasks(): Promise<void> {
|
||||||
|
const taskStartTime = Date.now();
|
||||||
try {
|
try {
|
||||||
logger.info(`Updating nodes and channels`);
|
logger.info(`Updating nodes and channels`);
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ class NetworkSyncService {
|
|||||||
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL);
|
setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -268,57 +268,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server": {
|
|
||||||
"builder": "@angular-devkit/build-angular:server",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/mempool/server",
|
|
||||||
"main": "server.ts",
|
|
||||||
"tsConfig": "tsconfig.server.json",
|
|
||||||
"sourceMap": true,
|
|
||||||
"optimization": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"outputHashing": "media",
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceMap": false,
|
|
||||||
"localize": true,
|
|
||||||
"optimization": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": ""
|
|
||||||
},
|
|
||||||
"serve-ssr": {
|
|
||||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "mempool:build",
|
|
||||||
"serverTarget": "mempool:server"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"browserTarget": "mempool:build:production",
|
|
||||||
"serverTarget": "mempool:server:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prerender": {
|
|
||||||
"builder": "@nguniversal/builders:prerender",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "mempool:build:production",
|
|
||||||
"serverTarget": "mempool:server:production",
|
|
||||||
"routes": [
|
|
||||||
"/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cypress-run": {
|
"cypress-run": {
|
||||||
"builder": "@cypress/schematic:cypress",
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
@ -339,6 +288,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"defaultProject": "mempool"
|
|
||||||
}
|
}
|
||||||
|
14552
frontend/package-lock.json
generated
14552
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -51,9 +51,6 @@
|
|||||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||||
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||||
"dev:ssr": "npm run generate-config && npm run ng -- run mempool:serve-ssr",
|
|
||||||
"serve:ssr": "node server.run.js",
|
|
||||||
"build:ssr": "npm run build && npm run ng -- run mempool:server:production && npm run tsc -- server.run.ts",
|
|
||||||
"prerender": "npm run ng -- run mempool:prerender",
|
"prerender": "npm run ng -- run mempool:prerender",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run",
|
"cypress:run": "cypress run",
|
||||||
@ -64,63 +61,59 @@
|
|||||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "~13.3.7",
|
"@angular-devkit/build-angular": "^14.2.10",
|
||||||
"@angular/animations": "~13.3.10",
|
"@angular/animations": "^14.2.12",
|
||||||
"@angular/cli": "~13.3.7",
|
"@angular/cli": "^14.2.10",
|
||||||
"@angular/common": "~13.3.10",
|
"@angular/common": "^14.2.12",
|
||||||
"@angular/compiler": "~13.3.10",
|
"@angular/compiler": "^14.2.12",
|
||||||
"@angular/core": "~13.3.10",
|
"@angular/core": "^14.2.12",
|
||||||
"@angular/forms": "~13.3.10",
|
"@angular/forms": "^14.2.12",
|
||||||
"@angular/localize": "~13.3.10",
|
"@angular/localize": "^14.2.12",
|
||||||
"@angular/platform-browser": "~13.3.10",
|
"@angular/platform-browser": "^14.2.12",
|
||||||
"@angular/platform-browser-dynamic": "~13.3.10",
|
"@angular/platform-browser-dynamic": "^14.2.12",
|
||||||
"@angular/platform-server": "~13.3.10",
|
"@angular/platform-server": "^14.2.12",
|
||||||
"@angular/router": "~13.3.10",
|
"@angular/router": "^14.2.12",
|
||||||
"@fortawesome/angular-fontawesome": "~0.10.2",
|
"@fortawesome/angular-fontawesome": "~0.11.1",
|
||||||
"@fortawesome/fontawesome-common-types": "~6.1.1",
|
"@fortawesome/fontawesome-common-types": "~6.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "~6.1.1",
|
"@fortawesome/fontawesome-svg-core": "~6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "~6.1.1",
|
"@fortawesome/free-solid-svg-icons": "~6.2.1",
|
||||||
"@mempool/mempool.js": "2.3.0",
|
"@mempool/mempool.js": "2.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^13.1.1",
|
||||||
"@nguniversal/express-engine": "~13.1.1",
|
"@types/qrcode": "~1.5.0",
|
||||||
"@types/qrcode": "~1.4.2",
|
"bootstrap": "~4.6.1",
|
||||||
"bootstrap": "~4.5.0",
|
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.10",
|
"clipboard": "^2.0.11",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.3.2",
|
"echarts": "~5.4.0",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"express": "^4.17.1",
|
|
||||||
"lightweight-charts": "~3.8.0",
|
"lightweight-charts": "~3.8.0",
|
||||||
"ngx-echarts": "8.0.1",
|
"ngx-echarts": "~14.0.0",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^14.0.1",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.5.5",
|
"rxjs": "~7.5.7",
|
||||||
"tinyify": "^3.0.0",
|
"tinyify": "^3.1.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "~2.4.1",
|
||||||
"zone.js": "~0.11.5"
|
"zone.js": "~0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "~13.3.10",
|
"@angular/compiler-cli": "^14.2.12",
|
||||||
"@angular/language-service": "~13.3.10",
|
"@angular/language-service": "^14.2.12",
|
||||||
"@nguniversal/builders": "~13.1.1",
|
"@types/node": "^18.11.9",
|
||||||
"@types/express": "^4.17.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@types/node": "^12.11.1",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
"eslint": "^8.28.0",
|
||||||
"@typescript-eslint/parser": "^5.30.5",
|
|
||||||
"eslint": "^8.19.0",
|
|
||||||
"http-proxy-middleware": "~2.0.6",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.0",
|
||||||
"ts-node": "~10.8.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "~4.6.4"
|
"typescript": "~4.6.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "~2.0.0",
|
"@cypress/schematic": "~2.3.0",
|
||||||
"cypress": "^10.3.0",
|
"cypress": "^11.2.0",
|
||||||
"cypress-fail-on-console-error": "~3.0.0",
|
"cypress-fail-on-console-error": "~4.0.2",
|
||||||
"cypress-wait-until": "^1.7.2",
|
"cypress-wait-until": "^1.7.2",
|
||||||
"mock-socket": "~9.1.4",
|
"mock-socket": "~9.1.5",
|
||||||
"start-server-and-test": "~1.14.0"
|
"start-server-and-test": "~1.14.0"
|
||||||
},
|
},
|
||||||
"scarfSettings": {
|
"scarfSettings": {
|
||||||
|
@ -3,9 +3,9 @@ const fs = require('fs');
|
|||||||
let PROXY_CONFIG = require('./proxy.conf');
|
let PROXY_CONFIG = require('./proxy.conf');
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
PROXY_CONFIG.forEach(entry => {
|
||||||
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
|
entry.target = entry.target.replace("mempool.space", "mempool-staging.tk7.mempool.space");
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
|
entry.target = entry.target.replace("liquid.network", "liquid-staging.tk7.mempool.space");
|
||||||
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
|
entry.target = entry.target.replace("bisq.markets", "bisq-staging.tk7.mempool.space");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = PROXY_CONFIG;
|
module.exports = PROXY_CONFIG;
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
import 'zone.js/node';
|
|
||||||
import './generated-config';
|
|
||||||
|
|
||||||
import * as domino from 'domino';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
const {readFileSync, existsSync} = require('fs');
|
|
||||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
|
||||||
|
|
||||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
|
||||||
const win = domino.createWindow(template);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.__env = global.__env;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.matchMedia = () => {
|
|
||||||
return {
|
|
||||||
matches: true
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.setTimeout = (fn) => { fn(); };
|
|
||||||
win.document.body.scrollTo = (() => {});
|
|
||||||
// @ts-ignore
|
|
||||||
global['window'] = win;
|
|
||||||
global['document'] = win.document;
|
|
||||||
// @ts-ignore
|
|
||||||
global['history'] = { state: { } };
|
|
||||||
|
|
||||||
global['localStorage'] = {
|
|
||||||
getItem: () => '',
|
|
||||||
setItem: () => {},
|
|
||||||
removeItem: () => {},
|
|
||||||
clear: () => {},
|
|
||||||
length: 0,
|
|
||||||
key: () => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the list of supported and actually active locales
|
|
||||||
*/
|
|
||||||
function getActiveLocales() {
|
|
||||||
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
|
|
||||||
|
|
||||||
const supportedLocales = [
|
|
||||||
angularConfig.projects.mempool.i18n.sourceLocale,
|
|
||||||
...Object.keys(angularConfig.projects.mempool.i18n.locales),
|
|
||||||
];
|
|
||||||
|
|
||||||
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function app() {
|
|
||||||
const server = express();
|
|
||||||
|
|
||||||
// proxy API to nginx
|
|
||||||
server.get('/api/**', createProxyMiddleware({
|
|
||||||
// @ts-ignore
|
|
||||||
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
|
|
||||||
changeOrigin: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// map / and /en to en-US
|
|
||||||
const defaultLocale = 'en-US';
|
|
||||||
console.log(`serving default locale: ${defaultLocale}`);
|
|
||||||
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
|
|
||||||
server.use('/', appServerModule.app(defaultLocale));
|
|
||||||
server.use('/en', appServerModule.app(defaultLocale));
|
|
||||||
|
|
||||||
// map each locale to its localized main.js
|
|
||||||
getActiveLocales().forEach(locale => {
|
|
||||||
console.log('serving locale:', locale);
|
|
||||||
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
|
|
||||||
|
|
||||||
// map everything to itself
|
|
||||||
server.use(`/${locale}`, appServerModule.app(locale));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
const port = process.env.PORT || 4000;
|
|
||||||
|
|
||||||
// Start up the Node server
|
|
||||||
app().listen(port, () => {
|
|
||||||
console.log(`Node Express server listening on port ${port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
@ -1,160 +0,0 @@
|
|||||||
import 'zone.js/node';
|
|
||||||
import './generated-config';
|
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as domino from 'domino';
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { AppServerModule } from './src/main.server';
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
|
||||||
const win = domino.createWindow(template);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.__env = global.__env;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.matchMedia = () => {
|
|
||||||
return {
|
|
||||||
matches: true
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
win.setTimeout = (fn) => { fn(); };
|
|
||||||
win.document.body.scrollTo = (() => {});
|
|
||||||
// @ts-ignore
|
|
||||||
global['window'] = win;
|
|
||||||
global['document'] = win.document;
|
|
||||||
// @ts-ignore
|
|
||||||
global['history'] = { state: { } };
|
|
||||||
|
|
||||||
global['localStorage'] = {
|
|
||||||
getItem: () => '',
|
|
||||||
setItem: () => {},
|
|
||||||
removeItem: () => {},
|
|
||||||
clear: () => {},
|
|
||||||
length: 0,
|
|
||||||
key: () => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
|
||||||
export function app(locale: string): express.Express {
|
|
||||||
const server = express();
|
|
||||||
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
|
|
||||||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
|
||||||
|
|
||||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
|
||||||
server.engine('html', ngExpressEngine({
|
|
||||||
bootstrap: AppServerModule,
|
|
||||||
}));
|
|
||||||
|
|
||||||
server.set('view engine', 'html');
|
|
||||||
server.set('views', distFolder);
|
|
||||||
|
|
||||||
// only handle URLs that actually exist
|
|
||||||
//server.get(locale, getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/mining/pool/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/liquid/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/testnet/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/mempool-block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/mining/pools', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/graphs', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/signet/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/bisq/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/about', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/api', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/tv', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/status', getLocalizedSSR(indexHtml));
|
|
||||||
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
|
|
||||||
|
|
||||||
// fallback to static file handler so we send HTTP 404 to nginx
|
|
||||||
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocalizedSSR(indexHtml) {
|
|
||||||
return (req, res) => {
|
|
||||||
res.render(indexHtml, {
|
|
||||||
req,
|
|
||||||
providers: [
|
|
||||||
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only used for development mode
|
|
||||||
function run(): void {
|
|
||||||
const port = process.env.PORT || 4000;
|
|
||||||
|
|
||||||
// Start up the Node server
|
|
||||||
const server = app('en-US');
|
|
||||||
server.listen(port, () => {
|
|
||||||
console.log(`Node Express server listening on port ${port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webpack will replace 'require' with '__webpack_require__'
|
|
||||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
|
||||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
|
||||||
declare const __non_webpack_require__: NodeRequire;
|
|
||||||
const mainModule = __non_webpack_require__.main;
|
|
||||||
const moduleFilename = mainModule && mainModule.filename || '';
|
|
||||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './src/main.server';
|
|
@ -4,7 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
|
|||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||||
import { BlockComponent } from './components/block/block.component';
|
import { BlockComponent } from './components/block/block.component';
|
||||||
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
|
|
||||||
import { AddressComponent } from './components/address/address.component';
|
import { AddressComponent } from './components/address/address.component';
|
||||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||||
import { AboutComponent } from './components/about/about.component';
|
import { AboutComponent } from './components/about/about.component';
|
||||||
@ -103,16 +102,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
data: { networkSpecific: true },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||||
@ -219,16 +208,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
data: { networkSpecific: true },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||||
@ -331,16 +310,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'block-audit',
|
|
||||||
data: { networkSpecific: true },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: BlockAuditComponent
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||||
@ -658,7 +627,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
initialNavigation: 'enabled',
|
initialNavigation: 'enabledBlocking',
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
anchorScrolling: 'enabled',
|
anchorScrolling: 'enabled',
|
||||||
preloadingStrategy: AppPreloadingStrategy
|
preloadingStrategy: AppPreloadingStrategy
|
||||||
|
@ -10,27 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
|
||||||
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M
|
<input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
|
||||||
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H
|
<input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
|
||||||
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H
|
<input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
|
||||||
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D
|
<input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
|
||||||
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W
|
<input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
|
||||||
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M
|
<input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
|
||||||
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y
|
<input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 1Y
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { combineLatest, merge, Observable, of } from 'rxjs';
|
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
@ -19,7 +19,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
|||||||
currency$: Observable<any>;
|
currency$: Observable<any>;
|
||||||
offers$: Observable<OffersMarket>;
|
offers$: Observable<OffersMarket>;
|
||||||
trades$: Observable<Trade[]>;
|
trades$: Observable<Trade[]>;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
defaultInterval = 'day';
|
defaultInterval = 'day';
|
||||||
|
|
||||||
isLoadingGraph = false;
|
isLoadingGraph = false;
|
||||||
@ -28,7 +28,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private bisqApiService: BisqApiService,
|
private bisqApiService: BisqApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) { }
|
) { }
|
||||||
|
@ -5,7 +5,7 @@ import { Observable, Subscription } from 'rxjs';
|
|||||||
import { switchMap, map, tap } from 'rxjs/operators';
|
import { switchMap, map, tap } from 'rxjs/operators';
|
||||||
import { BisqApiService } from '../bisq-api.service';
|
import { BisqApiService } from '../bisq-api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
|
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -23,7 +23,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
|||||||
fiveItemsPxSize = 250;
|
fiveItemsPxSize = 250;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
loadingItems: number[];
|
loadingItems: number[];
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
types: string[] = [];
|
types: string[] = [];
|
||||||
radioGroupSubscription: Subscription;
|
radioGroupSubscription: Subscription;
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private bisqApiService: BisqApiService,
|
private bisqApiService: BisqApiService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
|
@ -129,7 +129,7 @@
|
|||||||
<span>Gemini</span>
|
<span>Gemini</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||||
<svg width="81" height="81" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||||
<g clip-path="url(#clip0_2_14)">
|
<g clip-path="url(#clip0_2_14)">
|
||||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 81px;
|
width: 80px;
|
||||||
height: 81px;
|
height: 80px;
|
||||||
background-size: 100%, 100%;
|
background-size: 100%, 100%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 25px;
|
margin: 25px;
|
||||||
@ -191,6 +191,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.community-integrations-sponsor {
|
.community-integrations-sponsor {
|
||||||
max-width: 970px;
|
max-width: 965px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { merge, Observable, of, Subject } from 'rxjs';
|
import { merge, Observable, of, Subject } from 'rxjs';
|
||||||
@ -19,7 +19,7 @@ import { environment } from '../../../../environments/environment';
|
|||||||
export class AssetsNavComponent implements OnInit {
|
export class AssetsNavComponent implements OnInit {
|
||||||
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
|
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
|
||||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||||
searchForm: FormGroup;
|
searchForm: UntypedFormGroup;
|
||||||
assetsCache: AssetExtended[];
|
assetsCache: AssetExtended[];
|
||||||
|
|
||||||
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
|
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
|
||||||
@ -30,7 +30,7 @@ export class AssetsNavComponent implements OnInit {
|
|||||||
itemsPerPage = 15;
|
itemsPerPage = 15;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private assetsService: AssetsService,
|
private assetsService: AssetsService,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
@ -22,7 +22,7 @@ export class AssetsComponent implements OnInit {
|
|||||||
|
|
||||||
assets: AssetExtended[];
|
assets: AssetExtended[];
|
||||||
assetsCache: AssetExtended[];
|
assetsCache: AssetExtended[];
|
||||||
searchForm: FormGroup;
|
searchForm: UntypedFormGroup;
|
||||||
assets$: Observable<AssetExtended[]>;
|
assets$: Observable<AssetExtended[]>;
|
||||||
|
|
||||||
page = 1;
|
page = 1;
|
||||||
|
@ -30,7 +30,6 @@ export class BisqMasterPageComponent implements OnInit {
|
|||||||
this.connectionState$ = this.stateService.connectionState$;
|
this.connectionState$ = this.stateService.connectionState$;
|
||||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||||
console.log('network paths updated...');
|
|
||||||
this.networkPaths = paths;
|
this.networkPaths = paths;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,172 +0,0 @@
|
|||||||
<div class="container-xl" (window:resize)="onResize($event)">
|
|
||||||
|
|
||||||
<div class="title-block" id="block">
|
|
||||||
<h1>
|
|
||||||
<span class="next-previous-blocks">
|
|
||||||
<span i18n="shared.block-audit-title">Block Audit</span>
|
|
||||||
|
|
||||||
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="grow"></div>
|
|
||||||
|
|
||||||
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="!error && !isLoading">
|
|
||||||
|
|
||||||
|
|
||||||
<!-- OVERVIEW -->
|
|
||||||
<div class="box mb-3">
|
|
||||||
<div class="row">
|
|
||||||
<!-- LEFT COLUMN -->
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="block.hash">Hash</td>
|
|
||||||
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
|
|
||||||
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="blockAudit.timestamp">Timestamp</td>
|
|
||||||
<td>
|
|
||||||
‎{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
|
||||||
<div class="lg-inline">
|
|
||||||
<i class="symbol">(<app-time-since [time]="blockAudit.timestamp" [fastRender]="true">
|
|
||||||
</app-time-since>)</i>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="blockAudit.size">Size</td>
|
|
||||||
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.weight">Weight</td>
|
|
||||||
<td [innerHTML]="'‎' + (blockAudit.weight | wuBytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT COLUMN -->
|
|
||||||
<div class="col-sm" *ngIf="blockAudit">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
|
||||||
<td>{{ blockAudit.tx_count }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.health">Block health</td>
|
|
||||||
<td>{{ blockAudit.matchRate }}%</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.missing-txs">Removed txs</td>
|
|
||||||
<td>{{ blockAudit.missingTxs.length }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.added-txs">Added txs</td>
|
|
||||||
<td>{{ blockAudit.addedTxs.length }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div> <!-- row -->
|
|
||||||
</div> <!-- box -->
|
|
||||||
|
|
||||||
<!-- ADDED vs MISSING button -->
|
|
||||||
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
|
|
||||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
|
|
||||||
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
|
||||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
|
|
||||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template [ngIf]="!error && isLoading">
|
|
||||||
<!-- OVERVIEW -->
|
|
||||||
<div class="box mb-3">
|
|
||||||
<div class="row">
|
|
||||||
<!-- LEFT COLUMN -->
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT COLUMN -->
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div> <!-- row -->
|
|
||||||
</div> <!-- box -->
|
|
||||||
|
|
||||||
<!-- ADDED vs MISSING button -->
|
|
||||||
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
|
|
||||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
|
|
||||||
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
|
||||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
|
|
||||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template [ngIf]="error">
|
|
||||||
<div *ngIf="error && error.status === 404; else generalError" class="text-center">
|
|
||||||
<br>
|
|
||||||
<b i18n="error.audit-unavailable">audit unavailable</b>
|
|
||||||
<br><br>
|
|
||||||
<i>{{ error.error }}</i>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<ng-template #generalError>
|
|
||||||
<div class="text-center">
|
|
||||||
<br>
|
|
||||||
<span i18n="error.general-loading-data">Error loading data.</span>
|
|
||||||
<br><br>
|
|
||||||
<i>{{ error }}</i>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<!-- VISUALIZATIONS -->
|
|
||||||
<div class="box" *ngIf="!error">
|
|
||||||
<div class="row">
|
|
||||||
<!-- MISSING TX RENDERING -->
|
|
||||||
<div class="col-sm" *ngIf="webGlEnabled">
|
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
|
||||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
|
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ADDED TX RENDERING -->
|
|
||||||
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
|
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
|
||||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
|
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
|
||||||
</div>
|
|
||||||
</div> <!-- row -->
|
|
||||||
</div> <!-- box -->
|
|
||||||
|
|
||||||
</div>
|
|
@ -1,44 +0,0 @@
|
|||||||
.title-block {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
tr td {
|
|
||||||
&:last-child {
|
|
||||||
text-align: right;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-tx-title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
line-height: 1;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
padding-bottom: 0px;
|
|
||||||
align-self: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button {
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-subtitle {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
@ -1,229 +0,0 @@
|
|||||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
|
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
|
||||||
import { Subscription, combineLatest, of } from 'rxjs';
|
|
||||||
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
|
|
||||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
|
||||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-block-audit',
|
|
||||||
templateUrl: './block-audit.component.html',
|
|
||||||
styleUrls: ['./block-audit.component.scss'],
|
|
||||||
styles: [`
|
|
||||||
.loadingGraphs {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: calc(50% - 15px);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
`],
|
|
||||||
})
|
|
||||||
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
||||||
blockAudit: BlockAudit = undefined;
|
|
||||||
transactions: string[];
|
|
||||||
auditSubscription: Subscription;
|
|
||||||
urlFragmentSubscription: Subscription;
|
|
||||||
|
|
||||||
paginationMaxSize: number;
|
|
||||||
page = 1;
|
|
||||||
itemsPerPage: number;
|
|
||||||
|
|
||||||
mode: 'projected' | 'actual' = 'projected';
|
|
||||||
error: any;
|
|
||||||
isLoading = true;
|
|
||||||
webGlEnabled = true;
|
|
||||||
isMobile = window.innerWidth <= 767.98;
|
|
||||||
hoverTx: string;
|
|
||||||
|
|
||||||
childChangeSubscription: Subscription;
|
|
||||||
|
|
||||||
blockHash: string;
|
|
||||||
numMissing: number = 0;
|
|
||||||
numUnexpected: number = 0;
|
|
||||||
|
|
||||||
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
|
||||||
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
public stateService: StateService,
|
|
||||||
private router: Router,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private electrsApiService: ElectrsApiService,
|
|
||||||
) {
|
|
||||||
this.webGlEnabled = detectWebGL();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.childChangeSubscription.unsubscribe();
|
|
||||||
this.urlFragmentSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
|
||||||
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
|
||||||
|
|
||||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
|
||||||
if (fragment === 'actual') {
|
|
||||||
this.mode = 'actual';
|
|
||||||
} else {
|
|
||||||
this.mode = 'projected'
|
|
||||||
}
|
|
||||||
this.setupBlockGraphs();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.auditSubscription = this.route.paramMap.pipe(
|
|
||||||
switchMap((params: ParamMap) => {
|
|
||||||
const blockHash = params.get('id') || null;
|
|
||||||
if (!blockHash) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isBlockHeight = false;
|
|
||||||
if (/^[0-9]+$/.test(blockHash)) {
|
|
||||||
isBlockHeight = true;
|
|
||||||
} else {
|
|
||||||
this.blockHash = blockHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBlockHeight) {
|
|
||||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
|
||||||
.pipe(
|
|
||||||
switchMap((hash: string) => {
|
|
||||||
if (hash) {
|
|
||||||
this.blockHash = hash;
|
|
||||||
return this.apiService.getBlockAudit$(this.blockHash)
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
this.error = err;
|
|
||||||
return of(null);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.apiService.getBlockAudit$(this.blockHash)
|
|
||||||
}),
|
|
||||||
filter((response) => response != null),
|
|
||||||
map((response) => {
|
|
||||||
const blockAudit = response.body;
|
|
||||||
const inTemplate = {};
|
|
||||||
const inBlock = {};
|
|
||||||
const isAdded = {};
|
|
||||||
const isCensored = {};
|
|
||||||
const isMissing = {};
|
|
||||||
const isSelected = {};
|
|
||||||
this.numMissing = 0;
|
|
||||||
this.numUnexpected = 0;
|
|
||||||
for (const tx of blockAudit.template) {
|
|
||||||
inTemplate[tx.txid] = true;
|
|
||||||
}
|
|
||||||
for (const tx of blockAudit.transactions) {
|
|
||||||
inBlock[tx.txid] = true;
|
|
||||||
}
|
|
||||||
for (const txid of blockAudit.addedTxs) {
|
|
||||||
isAdded[txid] = true;
|
|
||||||
}
|
|
||||||
for (const txid of blockAudit.missingTxs) {
|
|
||||||
isCensored[txid] = true;
|
|
||||||
}
|
|
||||||
// set transaction statuses
|
|
||||||
for (const tx of blockAudit.template) {
|
|
||||||
if (isCensored[tx.txid]) {
|
|
||||||
tx.status = 'censored';
|
|
||||||
} else if (inBlock[tx.txid]) {
|
|
||||||
tx.status = 'found';
|
|
||||||
} else {
|
|
||||||
tx.status = 'missing';
|
|
||||||
isMissing[tx.txid] = true;
|
|
||||||
this.numMissing++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return blockAudit;
|
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
console.log(err);
|
|
||||||
this.error = err;
|
|
||||||
this.isLoading = false;
|
|
||||||
return of(null);
|
|
||||||
}),
|
|
||||||
).subscribe((blockAudit) => {
|
|
||||||
this.blockAudit = blockAudit;
|
|
||||||
this.setupBlockGraphs();
|
|
||||||
this.isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
|
|
||||||
this.setupBlockGraphs();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setupBlockGraphs() {
|
|
||||||
if (this.blockAudit) {
|
|
||||||
this.blockGraphProjected.forEach(graph => {
|
|
||||||
graph.destroy();
|
|
||||||
if (this.isMobile && this.mode === 'actual') {
|
|
||||||
graph.setup(this.blockAudit.transactions);
|
|
||||||
} else {
|
|
||||||
graph.setup(this.blockAudit.template);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.blockGraphActual.forEach(graph => {
|
|
||||||
graph.destroy();
|
|
||||||
graph.setup(this.blockAudit.transactions);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize(event: any) {
|
|
||||||
const isMobile = event.target.innerWidth <= 767.98;
|
|
||||||
const changed = isMobile !== this.isMobile;
|
|
||||||
this.isMobile = isMobile;
|
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
this.changeMode(this.mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMode(mode: 'projected' | 'actual') {
|
|
||||||
this.router.navigate([], { fragment: mode });
|
|
||||||
}
|
|
||||||
|
|
||||||
onTxClick(event: TransactionStripped): void {
|
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
|
||||||
this.router.navigate([url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTxHover(txid: string): void {
|
|
||||||
if (txid && txid.length) {
|
|
||||||
this.hoverTx = txid;
|
|
||||||
} else {
|
|
||||||
this.hoverTx = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,36 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@ -33,7 +33,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -50,7 +50,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
@ -10,27 +10,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@ -31,7 +31,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -48,7 +48,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<div class="block-overview-graph">
|
<div class="block-overview-graph">
|
||||||
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||||
<div class="loader-wrapper" [class.hidden]="!isLoading || disableSpinner">
|
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||||
<div class="spinner-border ml-3 loading" role="status"></div>
|
<div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
|
||||||
|
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-block-overview-tooltip
|
<app-block-overview-tooltip
|
||||||
|
@ -19,6 +19,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() flip = true;
|
@Input() flip = true;
|
||||||
@Input() disableSpinner = false;
|
@Input() disableSpinner = false;
|
||||||
@Input() mirrorTxid: string | void;
|
@Input() mirrorTxid: string | void;
|
||||||
|
@Input() unavailable: boolean = false;
|
||||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||||
@Output() txHoverEvent = new EventEmitter<string>();
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
@ -3,12 +3,13 @@ import { FastVertexArray } from './fast-vertex-array';
|
|||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
||||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||||
|
import BlockScene from './block-scene';
|
||||||
|
|
||||||
const hoverTransitionTime = 300;
|
const hoverTransitionTime = 300;
|
||||||
const defaultHoverColor = hexToColor('1bd8f4');
|
const defaultHoverColor = hexToColor('1bd8f4');
|
||||||
|
|
||||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||||
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
|
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||||
const auditColors = {
|
const auditColors = {
|
||||||
censored: hexToColor('f344df'),
|
censored: hexToColor('f344df'),
|
||||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||||
@ -34,7 +35,8 @@ export default class TxView implements TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
|
context?: 'projected' | 'actual';
|
||||||
|
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
@ -48,6 +50,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
||||||
|
this.context = tx.context;
|
||||||
this.txid = tx.txid;
|
this.txid = tx.txid;
|
||||||
this.fee = tx.fee;
|
this.fee = tx.fee;
|
||||||
this.vsize = tx.vsize;
|
this.vsize = tx.vsize;
|
||||||
@ -159,12 +162,18 @@ export default class TxView implements TransactionStripped {
|
|||||||
return auditColors.censored;
|
return auditColors.censored;
|
||||||
case 'missing':
|
case 'missing':
|
||||||
return auditColors.missing;
|
return auditColors.missing;
|
||||||
|
case 'fresh':
|
||||||
|
return auditColors.missing;
|
||||||
case 'added':
|
case 'added':
|
||||||
return auditColors.added;
|
return auditColors.added;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return auditColors.selected;
|
return auditColors.selected;
|
||||||
case 'found':
|
case 'found':
|
||||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
if (this.context === 'projected') {
|
||||||
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
|
} else {
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return feeLevelColor;
|
return feeLevelColor;
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,10 @@
|
|||||||
<ng-container [ngSwitch]="tx?.status">
|
<ng-container [ngSwitch]="tx?.status">
|
||||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
||||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
||||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</td>
|
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||||
|
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
|
||||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td>
|
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -10,36 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@ -31,7 +31,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -48,7 +48,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -11,27 +11,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
@ -31,7 +31,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -48,7 +48,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -9,36 +9,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@ -30,7 +30,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -49,7 +49,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div class="title-block" [class.time-ltr]="timeLtr" id="block">
|
<div class="title-block" [class.time-ltr]="timeLtr" id="block">
|
||||||
<h1>
|
<h1>
|
||||||
<ng-container *ngIf="blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
|
<ng-container *ngIf="blockHeight == null || blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
|
||||||
<ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
|
<ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
|
||||||
<span class="next-previous-blocks">
|
<span class="next-previous-blocks">
|
||||||
<a *ngIf="showNextBlocklink" class="nav-arrow next" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
<a *ngIf="showNextBlocklink" class="nav-arrow next" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
||||||
@ -54,7 +54,19 @@
|
|||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="webGlEnabled">
|
<tr *ngIf="auditEnabled">
|
||||||
|
<td i18n="block.health">Block health</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
|
||||||
|
<span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
|
||||||
|
<tr *ngIf="isMobile && auditEnabled"></tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||||
|
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||||
|
</tr>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
@ -98,26 +110,19 @@
|
|||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block.extras.pool.name }}
|
{{ block.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
<span placement="bottom" class="badge"
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block.extras.pool.name }}
|
{{ block.extras.pool.name }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="indexingAvailable">
|
</ng-container>
|
||||||
<td i18n="block.health">Block health</td>
|
|
||||||
<td>
|
|
||||||
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
|
|
||||||
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +143,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="webGlEnabled">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
|
||||||
|
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -148,17 +157,25 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="col-sm" *ngIf="!webGlEnabled">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock">
|
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (indexingAvailable || !webGlEnabled)">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile && auditEnabled"></tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||||
|
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||||
|
</tr>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
@ -216,8 +233,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock">
|
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock && (indexingAvailable || !webGlEnabled)">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -230,22 +248,54 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<div class="col-sm chart-container" *ngIf="webGlEnabled && !indexingAvailable">
|
||||||
<div class="col-sm chart-container" *ngIf="webGlEnabled">
|
<app-block-overview-graph
|
||||||
<app-block-overview-graph
|
#blockGraphActual
|
||||||
#blockGraph
|
[isLoading]="isLoadingOverview"
|
||||||
[isLoading]="isLoadingOverview"
|
[resolution]="75"
|
||||||
[resolution]="75"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[orientation]="'top'"
|
||||||
[orientation]="'top'"
|
[flip]="false"
|
||||||
[flip]="false"
|
(txClickEvent)="onTxClick($event)"
|
||||||
(txClickEvent)="onTxClick($event)"
|
></app-block-overview-graph>
|
||||||
></app-block-overview-graph>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span id="overview"></span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<!-- VISUALIZATIONS -->
|
||||||
|
<div class="box" *ngIf="!error && webGlEnabled && indexingAvailable">
|
||||||
|
<div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
|
||||||
|
<a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
|
||||||
|
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
||||||
|
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||||
|
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
||||||
|
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm" *ngIf="!isMobile">
|
||||||
|
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
||||||
|
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||||
<div [hidden]="!showDetails" id="details">
|
<div [hidden]="!showDetails" id="details">
|
||||||
<br>
|
<br>
|
||||||
@ -273,6 +323,7 @@
|
|||||||
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="isMobile"></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="block.difficulty">Difficulty</td>
|
<td class="td-width" i18n="block.difficulty">Difficulty</td>
|
||||||
<td>{{ block.difficulty }}</td>
|
<td>{{ block.difficulty }}</td>
|
||||||
|
@ -171,3 +171,35 @@ h1 {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-subtitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border-color: white;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
background: inherit;
|
||||||
|
border-width: 1px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-color: transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #24273e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active, &:hover {
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||||
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 } from 'rxjs/operators';
|
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
|
||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs';
|
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } 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';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
@ -17,11 +17,20 @@ import { detectWebGL } from '../../shared/graphs.utils';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
templateUrl: './block.component.html',
|
templateUrl: './block.component.html',
|
||||||
styleUrls: ['./block.component.scss']
|
styleUrls: ['./block.component.scss'],
|
||||||
|
styles: [`
|
||||||
|
.loadingGraphs {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: calc(50% - 15px);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
`],
|
||||||
})
|
})
|
||||||
export class BlockComponent implements OnInit, OnDestroy {
|
export class BlockComponent implements OnInit, OnDestroy {
|
||||||
network = '';
|
network = '';
|
||||||
block: BlockExtended;
|
block: BlockExtended;
|
||||||
|
blockAudit: BlockAudit = undefined;
|
||||||
blockHeight: number;
|
blockHeight: number;
|
||||||
lastBlockHeight: number;
|
lastBlockHeight: number;
|
||||||
nextBlockHeight: number;
|
nextBlockHeight: number;
|
||||||
@ -48,9 +57,16 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
overviewError: any = null;
|
overviewError: any = null;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
indexingAvailable = false;
|
indexingAvailable = false;
|
||||||
|
auditEnabled = true;
|
||||||
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
hoverTx: string;
|
||||||
|
numMissing: number = 0;
|
||||||
|
numUnexpected: number = 0;
|
||||||
|
mode: 'projected' | 'actual' = 'projected';
|
||||||
|
|
||||||
transactionSubscription: Subscription;
|
transactionSubscription: Subscription;
|
||||||
overviewSubscription: Subscription;
|
overviewSubscription: Subscription;
|
||||||
|
auditSubscription: Subscription;
|
||||||
keyNavigationSubscription: Subscription;
|
keyNavigationSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
@ -60,10 +76,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
nextBlockTxListSubscription: Subscription = undefined;
|
nextBlockTxListSubscription: Subscription = undefined;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
fetchAuditScore$ = new Subject<string>();
|
childChangeSubscription: Subscription;
|
||||||
fetchAuditScoreSubscription: Subscription;
|
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -89,8 +105,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.timeLtr = !!ltr;
|
this.timeLtr = !!ltr;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
|
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
|
||||||
this.stateService.env.MINING_DASHBOARD === true);
|
this.auditEnabled = this.indexingAvailable;
|
||||||
|
|
||||||
this.txsLoadingStatus$ = this.route.paramMap
|
this.txsLoadingStatus$ = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -107,30 +123,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (block.id === this.blockHash) {
|
if (block.id === this.blockHash) {
|
||||||
this.block = block;
|
this.block = block;
|
||||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
|
||||||
this.fetchAuditScore$.next(this.block.id);
|
|
||||||
}
|
|
||||||
if (block?.extras?.reward != undefined) {
|
if (block?.extras?.reward != undefined) {
|
||||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.indexingAvailable) {
|
|
||||||
this.fetchAuditScoreSubscription = this.fetchAuditScore$
|
|
||||||
.pipe(
|
|
||||||
switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
|
|
||||||
catchError(() => EMPTY),
|
|
||||||
)
|
|
||||||
.subscribe((score) => {
|
|
||||||
if (score && score.hash === this.block.id) {
|
|
||||||
this.block.extras.matchRate = score.matchRate || null;
|
|
||||||
} else {
|
|
||||||
this.block.extras.matchRate = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const block$ = this.route.paramMap.pipe(
|
const block$ = this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
const blockHash: string = params.get('id') || '';
|
const blockHash: string = params.get('id') || '';
|
||||||
@ -212,7 +210,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
||||||
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
||||||
this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe();
|
this.apiService.getBlockAudit$(block.previousblockhash);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,9 +227,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||||
}
|
}
|
||||||
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
|
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
|
||||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
|
||||||
this.fetchAuditScore$.next(this.block.id);
|
|
||||||
}
|
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
this.transactionsError = null;
|
this.transactionsError = null;
|
||||||
@ -263,40 +258,126 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.overviewSubscription = block$.pipe(
|
if (!this.indexingAvailable) {
|
||||||
startWith(null),
|
this.overviewSubscription = block$.pipe(
|
||||||
pairwise(),
|
startWith(null),
|
||||||
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
pairwise(),
|
||||||
.pipe(
|
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
||||||
catchError((err) => {
|
.pipe(
|
||||||
this.overviewError = err;
|
catchError((err) => {
|
||||||
return of([]);
|
this.overviewError = err;
|
||||||
}),
|
return of([]);
|
||||||
switchMap((transactions) => {
|
}),
|
||||||
if (prevBlock) {
|
switchMap((transactions) => {
|
||||||
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
|
if (prevBlock) {
|
||||||
} else {
|
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
|
||||||
return of({ transactions, direction: 'down' });
|
} 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.indexingAvailable) {
|
||||||
|
this.auditSubscription = block$.pipe(
|
||||||
|
startWith(null),
|
||||||
|
pairwise(),
|
||||||
|
switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.overviewError = err;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
filter((response) => response != null),
|
||||||
|
map((response) => {
|
||||||
|
const blockAudit = response.body;
|
||||||
|
const inTemplate = {};
|
||||||
|
const inBlock = {};
|
||||||
|
const isAdded = {};
|
||||||
|
const isCensored = {};
|
||||||
|
const isMissing = {};
|
||||||
|
const isSelected = {};
|
||||||
|
const isFresh = {};
|
||||||
|
this.numMissing = 0;
|
||||||
|
this.numUnexpected = 0;
|
||||||
|
|
||||||
|
if (blockAudit?.template) {
|
||||||
|
for (const tx of blockAudit.template) {
|
||||||
|
inTemplate[tx.txid] = true;
|
||||||
}
|
}
|
||||||
})
|
for (const tx of blockAudit.transactions) {
|
||||||
)
|
inBlock[tx.txid] = true;
|
||||||
),
|
}
|
||||||
)
|
for (const txid of blockAudit.addedTxs) {
|
||||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
isAdded[txid] = true;
|
||||||
this.strippedTransactions = transactions;
|
}
|
||||||
this.isLoadingOverview = false;
|
for (const txid of blockAudit.missingTxs) {
|
||||||
if (this.blockGraph) {
|
isCensored[txid] = true;
|
||||||
this.blockGraph.destroy();
|
}
|
||||||
this.blockGraph.setup(this.strippedTransactions);
|
for (const txid of blockAudit.freshTxs || []) {
|
||||||
}
|
isFresh[txid] = true;
|
||||||
},
|
}
|
||||||
(error) => {
|
// set transaction statuses
|
||||||
this.error = error;
|
for (const tx of blockAudit.template) {
|
||||||
this.isLoadingOverview = false;
|
tx.context = 'projected';
|
||||||
if (this.blockGraph) {
|
if (isCensored[tx.txid]) {
|
||||||
this.blockGraph.destroy();
|
tx.status = 'censored';
|
||||||
}
|
} else if (inBlock[tx.txid]) {
|
||||||
});
|
tx.status = 'found';
|
||||||
|
} else {
|
||||||
|
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
|
||||||
|
isMissing[tx.txid] = true;
|
||||||
|
this.numMissing++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||||
|
tx.context = 'actual';
|
||||||
|
if (index === 0) {
|
||||||
|
tx.status = null;
|
||||||
|
} else if (isAdded[tx.txid]) {
|
||||||
|
tx.status = 'added';
|
||||||
|
} else if (inTemplate[tx.txid]) {
|
||||||
|
tx.status = 'found';
|
||||||
|
} else {
|
||||||
|
tx.status = 'selected';
|
||||||
|
isSelected[tx.txid] = true;
|
||||||
|
this.numUnexpected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const tx of blockAudit.transactions) {
|
||||||
|
inBlock[tx.txid] = true;
|
||||||
|
}
|
||||||
|
this.auditEnabled = true;
|
||||||
|
} else {
|
||||||
|
this.auditEnabled = false;
|
||||||
|
}
|
||||||
|
return blockAudit;
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
console.log(err);
|
||||||
|
this.error = err;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
).subscribe((blockAudit) => {
|
||||||
|
this.blockAudit = blockAudit;
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
@ -307,6 +388,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.showDetails = false;
|
this.showDetails = false;
|
||||||
}
|
}
|
||||||
|
if (params.view === 'projected') {
|
||||||
|
this.mode = 'projected';
|
||||||
|
} else {
|
||||||
|
this.mode = 'actual';
|
||||||
|
}
|
||||||
|
this.setupBlockGraphs();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||||
@ -325,17 +412,24 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.transactionSubscription.unsubscribe();
|
this.transactionSubscription.unsubscribe();
|
||||||
this.overviewSubscription.unsubscribe();
|
this.overviewSubscription?.unsubscribe();
|
||||||
|
this.auditSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription.unsubscribe();
|
this.keyNavigationSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.networkChangedSubscription.unsubscribe();
|
this.networkChangedSubscription.unsubscribe();
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.fetchAuditScoreSubscription?.unsubscribe();
|
|
||||||
this.unsubscribeNextBlockSubscriptions();
|
this.unsubscribeNextBlockSubscriptions();
|
||||||
|
this.childChangeSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribeNextBlockSubscriptions() {
|
unsubscribeNextBlockSubscriptions() {
|
||||||
@ -382,7 +476,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.showDetails = false;
|
this.showDetails = false;
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { showDetails: false },
|
queryParams: { showDetails: false, view: this.mode },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
fragment: 'block'
|
fragment: 'block'
|
||||||
});
|
});
|
||||||
@ -390,7 +484,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.showDetails = true;
|
this.showDetails = true;
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { showDetails: true },
|
queryParams: { showDetails: true, view: this.mode },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
fragment: 'details'
|
fragment: 'details'
|
||||||
});
|
});
|
||||||
@ -409,10 +503,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
|
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize(event: any) {
|
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToPreviousBlock() {
|
navigateToPreviousBlock() {
|
||||||
if (!this.block) {
|
if (!this.block) {
|
||||||
return;
|
return;
|
||||||
@ -443,8 +533,53 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupBlockGraphs(): void {
|
||||||
|
if (this.blockAudit || this.strippedTransactions) {
|
||||||
|
this.blockGraphProjected.forEach(graph => {
|
||||||
|
graph.destroy();
|
||||||
|
if (this.isMobile && this.mode === 'actual') {
|
||||||
|
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
|
||||||
|
} else {
|
||||||
|
graph.setup(this.blockAudit?.template || []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.blockGraphActual.forEach(graph => {
|
||||||
|
graph.destroy();
|
||||||
|
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(event: any): void {
|
||||||
|
const isMobile = event.target.innerWidth <= 767.98;
|
||||||
|
const changed = isMobile !== this.isMobile;
|
||||||
|
this.isMobile = isMobile;
|
||||||
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.changeMode(this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMode(mode: 'projected' | 'actual'): void {
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: { showDetails: this.showDetails, view: mode },
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
fragment: 'overview'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(event: TransactionStripped): void {
|
onTxClick(event: TransactionStripped): void {
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
||||||
this.router.navigate([url]);
|
this.router.navigate([url]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTxHover(txid: string): void {
|
||||||
|
if (txid && txid.length) {
|
||||||
|
this.hoverTx = txid;
|
||||||
|
} else {
|
||||||
|
this.hoverTx = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -46,14 +46,13 @@
|
|||||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
|
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block/' | relativeUrl, block.id] : null">
|
||||||
<div class="progress progress-health">
|
<div class="progress progress-health">
|
||||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||||
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
|
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
|
||||||
<div class="progress-text">
|
<div class="progress-text">
|
||||||
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
|
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
|
||||||
<span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span>
|
<span *ngIf="auditScores[block.id] == null">~</span>
|
||||||
<span *ngIf="auditScores[block.id] === null">~</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -31,24 +31,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3m'"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3m'" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'6m'"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'all'"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
|||||||
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 { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { selectPowerOfTen } from '../../bitcoin.utils';
|
import { selectPowerOfTen } from '../../bitcoin.utils';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@ -34,7 +34,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -54,7 +54,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -11,21 +11,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'6m'"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'all'"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
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 { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { poolsColor } from '../../app.constants';
|
import { poolsColor } from '../../app.constants';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
@ -30,7 +30,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||||||
@Input() left: number | string = 25;
|
@Input() left: number | string = 25;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -48,7 +48,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { languages } from '../../app.constants';
|
import { languages } from '../../app.constants';
|
||||||
import { LanguageService } from '../../services/language.service';
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
|
||||||
@ -11,12 +11,12 @@ import { LanguageService } from '../../services/language.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class LanguageSelectorComponent implements OnInit {
|
export class LanguageSelectorComponent implements OnInit {
|
||||||
languageForm: FormGroup;
|
languageForm: UntypedFormGroup;
|
||||||
languages = languages;
|
languages = languages;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private languageService: LanguageService,
|
private languageService: LanguageService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ export class LiquidMasterPageComponent implements OnInit {
|
|||||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||||
console.log('network paths updated...');
|
|
||||||
this.networkPaths = paths;
|
this.networkPaths = paths;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@ export class MasterPageComponent implements OnInit {
|
|||||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||||
this.subdomain = this.enterpriseService.getSubdomain();
|
this.subdomain = this.enterpriseService.getSubdomain();
|
||||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||||
console.log('network paths updated...');
|
|
||||||
this.networkPaths = paths;
|
this.networkPaths = paths;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
AbstractControl,
|
AbstractControl,
|
||||||
ControlValueAccessor,
|
ControlValueAccessor,
|
||||||
FormBuilder,
|
UntypedFormBuilder,
|
||||||
FormControl,
|
UntypedFormControl,
|
||||||
NG_VALUE_ACCESSOR,
|
NG_VALUE_ACCESSOR,
|
||||||
Validator,
|
Validator,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
@ -52,7 +52,7 @@ export class NgxDropdownMultiselectComponent implements OnInit,
|
|||||||
private localIsVisible = false;
|
private localIsVisible = false;
|
||||||
private workerDocClicked = false;
|
private workerDocClicked = false;
|
||||||
|
|
||||||
filterControl: FormControl = this.fb.control('');
|
filterControl: UntypedFormControl = this.fb.control('');
|
||||||
|
|
||||||
@Input() options: Array<IMultiSelectOption>;
|
@Input() options: Array<IMultiSelectOption>;
|
||||||
@Input() settings: IMultiSelectSettings;
|
@Input() settings: IMultiSelectSettings;
|
||||||
@ -151,7 +151,7 @@ export class NgxDropdownMultiselectComponent implements OnInit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fb: FormBuilder,
|
private fb: UntypedFormBuilder,
|
||||||
private searchFilter: MultiSelectSearchFilter,
|
private searchFilter: MultiSelectSearchFilter,
|
||||||
differs: IterableDiffers,
|
differs: IterableDiffers,
|
||||||
private cdRef: ChangeDetectorRef
|
private cdRef: ChangeDetectorRef
|
||||||
|
@ -40,36 +40,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
||||||
*ngIf="!widget && (miningStatsObservable$ | async) as stats">
|
*ngIf="!widget && (miningStatsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 144">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'24h'"> 24h
|
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'24h'" formControlName="dateSpan"> 24h
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 432">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3d'"> 3D
|
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3d'" formControlName="dateSpan"> 3D
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 1008">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1w'"> 1W
|
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1w'" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 4320">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1m'"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1m'" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 12960">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3m'"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3m'" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 25920">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'6m'"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 52560">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 105120">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'all'"><span i18n>All</span>
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"><span i18n>All</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||||
import { concat, Observable } from 'rxjs';
|
import { concat, Observable } from 'rxjs';
|
||||||
@ -24,7 +24,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
@Input() widget = false;
|
@Input() widget = false;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
@ -41,7 +41,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -8,13 +8,13 @@ import { ApiService } from '../../services/api.service';
|
|||||||
styleUrls: ['./push-transaction.component.scss']
|
styleUrls: ['./push-transaction.component.scss']
|
||||||
})
|
})
|
||||||
export class PushTransactionComponent implements OnInit {
|
export class PushTransactionComponent implements OnInit {
|
||||||
pushTxForm: FormGroup;
|
pushTxForm: UntypedFormGroup;
|
||||||
error: string = '';
|
error: string = '';
|
||||||
txId: string = '';
|
txId: string = '';
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
import { AssetsService } from '../../services/assets.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
@ -22,7 +22,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
isSearching = false;
|
isSearching = false;
|
||||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||||
typeAhead$: Observable<any>;
|
typeAhead$: Observable<any>;
|
||||||
searchForm: FormGroup;
|
searchForm: UntypedFormGroup;
|
||||||
dropdownHidden = false;
|
dropdownHidden = false;
|
||||||
|
|
||||||
@HostListener('document:click', ['$event'])
|
@HostListener('document:click', ['$event'])
|
||||||
@ -48,7 +48,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private assetsService: AssetsService,
|
private assetsService: AssetsService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
@ -13,46 +13,46 @@
|
|||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
||||||
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
||||||
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm mr-2">
|
<label class="btn btn-primary btn-sm mr-2">
|
||||||
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
||||||
<input ngbButton type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h"> 2H
|
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
|
||||||
(LIVE)
|
(LIVE)
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
|
||||||
<input ngbButton type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h">
|
<input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
|
||||||
24H
|
24H
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
|
||||||
<input ngbButton type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w"> 1W
|
<input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m"> 1M
|
<input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m"> 3M
|
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m"> 6M
|
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y"> 1Y
|
<input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y"> 2Y
|
<input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y"> 3Y
|
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-buttons">
|
<div class="small-buttons">
|
||||||
<div ngbDropdown #myDrop="ngbDropdown">
|
<div ngbDropdown #myDrop="ngbDropdown">
|
||||||
<button class="btn btn-primary btn-sm" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()">
|
<button class="btn btn btn-primary btn-sm" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()">
|
||||||
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title"
|
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title"
|
||||||
title="Filter"></fa-icon>
|
title="Filter"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button (click)="invertGraph()" class="btn btn-primary btn-sm">
|
<button (click)="invertGraph()" class="btn btn btn-primary btn-sm">
|
||||||
<fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true"
|
<fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true"
|
||||||
i18n-title="statistics.component-invert.title" title="Invert"></fa-icon>
|
i18n-title="statistics.component-invert.title" title="Invert"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core';
|
import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
|
||||||
import { of, merge} from 'rxjs';
|
import { of, merge} from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export class StatisticsComponent implements OnInit {
|
|||||||
mempoolUnconfirmedTransactionsData: any;
|
mempoolUnconfirmedTransactionsData: any;
|
||||||
mempoolTransactionsWeightPerSecondData: any;
|
mempoolTransactionsWeightPerSecondData: any;
|
||||||
|
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
graphWindowPreference: string;
|
graphWindowPreference: string;
|
||||||
inverted: boolean;
|
inverted: boolean;
|
||||||
feeLevelDropdownData = [];
|
feeLevelDropdownData = [];
|
||||||
@ -47,7 +47,7 @@ export class StatisticsComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||||
<app-tx-features [tx]="tx"></app-tx-features>
|
<app-tx-features [tx]="tx"></app-tx-features>
|
||||||
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
<span *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.descendants.length)" class="badge badge-primary mr-1">
|
||||||
CPFP
|
CPFP
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && cpfpInfo.ancestors.length" class="badge badge-info mr-1">
|
<span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.descendants.length && cpfpInfo.ancestors.length" class="badge badge-info mr-1">
|
||||||
CPFP
|
CPFP
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,34 +63,14 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((txId) =>
|
switchMap((txId) =>
|
||||||
this.apiService
|
this.apiService.getCpfpinfo$(txId).pipe(
|
||||||
.getCpfpinfo$(txId)
|
catchError((err) => {
|
||||||
.pipe(retryWhen((errors) => errors.pipe(delay(2000))))
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subscribe((cpfpInfo) => {
|
.subscribe((cpfpInfo) => {
|
||||||
if (!this.tx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
|
||||||
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
|
||||||
);
|
|
||||||
let totalWeight =
|
|
||||||
this.tx.weight +
|
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
|
||||||
let totalFees =
|
|
||||||
this.tx.fee +
|
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
|
||||||
|
|
||||||
if (cpfpInfo.bestDescendant) {
|
|
||||||
totalWeight += cpfpInfo.bestDescendant.weight;
|
|
||||||
totalFees += cpfpInfo.bestDescendant.fee;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
|
||||||
this.stateService.markBlock$.next({
|
|
||||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
|
||||||
});
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
||||||
});
|
});
|
||||||
@ -176,8 +156,17 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.getTransactionTime();
|
this.getTransactionTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.tx.status.confirmed) {
|
if (this.tx.status.confirmed) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
blockHeight: tx.status.block_height,
|
||||||
|
});
|
||||||
|
this.openGraphService.waitFor('cpfp-data-' + this.txId);
|
||||||
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
|
} else {
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txFeePerVSize: tx.effectiveFeePerVsize,
|
||||||
|
});
|
||||||
this.cpfpInfo = {
|
this.cpfpInfo = {
|
||||||
ancestors: tx.ancestors,
|
ancestors: tx.ancestors,
|
||||||
bestDescendant: tx.bestDescendant,
|
bestDescendant: tx.bestDescendant,
|
||||||
|
@ -156,7 +156,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template [ngIf]="cpfpInfo.bestDescendant">
|
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
|
||||||
|
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
|
||||||
|
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
|
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||||
|
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
|
||||||
|
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||||
|
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
|
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
<td>
|
<td>
|
||||||
@ -170,7 +183,7 @@
|
|||||||
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="cpfpInfo.ancestors.length">
|
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
|
||||||
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
||||||
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
||||||
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||||
@ -468,11 +481,11 @@
|
|||||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
<ng-template [ngIf]="tx.status.confirmed">
|
||||||
|
|
||||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.ancestors.length)">
|
<tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
|
||||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
|
@ -7,10 +7,11 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
retryWhen,
|
retryWhen,
|
||||||
delay,
|
delay,
|
||||||
map
|
map,
|
||||||
|
mergeMap
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
@ -110,32 +111,51 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
switchMap((txId) =>
|
switchMap((txId) =>
|
||||||
this.apiService
|
this.apiService
|
||||||
.getCpfpinfo$(txId)
|
.getCpfpinfo$(txId)
|
||||||
.pipe(retryWhen((errors) => errors.pipe(delay(2000))))
|
.pipe(retryWhen((errors) => errors.pipe(
|
||||||
)
|
mergeMap((error) => {
|
||||||
|
if (!this.tx?.status || this.tx.status.confirmed) {
|
||||||
|
return throwError(error);
|
||||||
|
} else {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
delay(2000)
|
||||||
|
)))
|
||||||
|
),
|
||||||
|
catchError(() => {
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.subscribe((cpfpInfo) => {
|
.subscribe((cpfpInfo) => {
|
||||||
if (!this.tx) {
|
if (!cpfpInfo || !this.tx) {
|
||||||
|
this.cpfpInfo = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
if (cpfpInfo.effectiveFeePerVsize) {
|
||||||
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||||
);
|
} else {
|
||||||
let totalWeight =
|
const lowerFeeParents = cpfpInfo.ancestors.filter(
|
||||||
this.tx.weight +
|
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
);
|
||||||
let totalFees =
|
let totalWeight =
|
||||||
this.tx.fee +
|
this.tx.weight +
|
||||||
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
|
||||||
|
let totalFees =
|
||||||
|
this.tx.fee +
|
||||||
|
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
|
||||||
|
|
||||||
if (cpfpInfo.bestDescendant) {
|
if (cpfpInfo?.bestDescendant) {
|
||||||
totalWeight += cpfpInfo.bestDescendant.weight;
|
totalWeight += cpfpInfo?.bestDescendant.weight;
|
||||||
totalFees += cpfpInfo.bestDescendant.fee;
|
totalFees += cpfpInfo?.bestDescendant.fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||||
|
}
|
||||||
|
if (!this.tx.status.confirmed) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
|
||||||
this.stateService.markBlock$.next({
|
|
||||||
txFeePerVSize: this.tx.effectiveFeePerVsize,
|
|
||||||
});
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -239,6 +259,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
blockHeight: tx.status.block_height,
|
blockHeight: tx.status.block_height,
|
||||||
});
|
});
|
||||||
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
} else {
|
} else {
|
||||||
if (tx.cpfpChecked) {
|
if (tx.cpfpChecked) {
|
||||||
this.stateService.markBlock$.next({
|
this.stateService.markBlock$.next({
|
||||||
|
@ -31,6 +31,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
|
|||||||
routes = [
|
routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
redirectTo: 'faq'
|
redirectTo: 'faq'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -139,6 +139,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
redirectTo: 'mempool',
|
redirectTo: 'mempool',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -22,7 +22,9 @@ interface BestDescendant {
|
|||||||
|
|
||||||
export interface CpfpInfo {
|
export interface CpfpInfo {
|
||||||
ancestors: Ancestor[];
|
ancestors: Ancestor[];
|
||||||
bestDescendant: BestDescendant | null;
|
descendants?: Ancestor[];
|
||||||
|
bestDescendant?: BestDescendant | null;
|
||||||
|
effectiveFeePerVsize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DifficultyAdjustment {
|
export interface DifficultyAdjustment {
|
||||||
@ -141,7 +143,7 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
@ -215,8 +217,8 @@ export interface IChannel {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
created: string;
|
created: string;
|
||||||
status: number;
|
status: number;
|
||||||
node_left: Node,
|
node_left: INode,
|
||||||
node_right: Node,
|
node_right: INode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -234,4 +236,6 @@ export interface INode {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
|
funding_balance?: number;
|
||||||
|
closing_balance?: number;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBackendInfo {
|
export interface IBackendInfo {
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<div class="box">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
|
||||||
|
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
|
||||||
|
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||||
|
<td *ngIf="!showStartingBalance">?</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="channel.status === 2">
|
||||||
|
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
|
||||||
|
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
|
||||||
|
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||||
|
<td *ngIf="!showClosingBalance">?</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
.box {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChannelCloseBoxComponent } from './channel-close-box.component';
|
||||||
|
|
||||||
|
describe('ChannelCloseBoxComponent', () => {
|
||||||
|
let component: ChannelCloseBoxComponent;
|
||||||
|
let fixture: ComponentFixture<ChannelCloseBoxComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ChannelCloseBoxComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChannelCloseBoxComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-channel-close-box',
|
||||||
|
templateUrl: './channel-close-box.component.html',
|
||||||
|
styleUrls: ['./channel-close-box.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ChannelCloseBoxComponent implements OnChanges {
|
||||||
|
@Input() channel: any;
|
||||||
|
@Input() local: any;
|
||||||
|
@Input() remote: any;
|
||||||
|
|
||||||
|
showStartingBalance: boolean = false;
|
||||||
|
showClosingBalance: boolean = false;
|
||||||
|
minStartingBalance: number;
|
||||||
|
maxStartingBalance: number;
|
||||||
|
minClosingBalance: number;
|
||||||
|
maxClosingBalance: number;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (this.channel && this.local && this.remote) {
|
||||||
|
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
|
||||||
|
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
|
||||||
|
|
||||||
|
if (this.channel.single_funded) {
|
||||||
|
if (this.local.funding_balance) {
|
||||||
|
this.minStartingBalance = this.channel.capacity;
|
||||||
|
this.maxStartingBalance = this.channel.capacity;
|
||||||
|
} else if (this.remote.funding_balance) {
|
||||||
|
this.minStartingBalance = 0;
|
||||||
|
this.maxStartingBalance = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
|
||||||
|
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
|
||||||
|
}
|
||||||
|
|
||||||
|
const closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||||
|
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
|
||||||
|
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
|
||||||
|
|
||||||
|
// margin of error to account for 2 x 330 sat anchor outputs
|
||||||
|
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
|
||||||
|
this.maxClosingBalance = this.minClosingBalance;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showStartingBalance = false;
|
||||||
|
this.showClosingBalance = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampRound(min: number, max: number, value: number): number {
|
||||||
|
return Math.max(0, Math.min(max, Math.round(value)));
|
||||||
|
}
|
@ -48,6 +48,15 @@
|
|||||||
<td i18n="lightning.capacity">Capacity</td>
|
<td i18n="lightning.capacity">Capacity</td>
|
||||||
<td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td>
|
<td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="channel.closed_by">
|
||||||
|
<td i18n="lightning.closed_by">Closed by</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/lightning/node' | relativeUrl, channel.closed_by]" >
|
||||||
|
<ng-container *ngIf="channel.closed_by === channel.node_left.public_key">{{ channel.node_left.alias }}</ng-container>
|
||||||
|
<ng-container *ngIf="channel.closed_by === channel.node_right.public_key">{{ channel.node_right.alias }}</ng-container>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -59,9 +68,11 @@
|
|||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
||||||
|
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||||
|
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -78,4 +78,9 @@ export class ChannelComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCloseBoxes(channel: IChannel): boolean {
|
||||||
|
return !!(channel.node_left.funding_balance || channel.node_left.closing_balance
|
||||||
|
|| channel.node_right.funding_balance || channel.node_right.closing_balance);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<div *ngIf="channels$ | async as response; else skeleton" style="position: relative;">
|
<div *ngIf="channels$ | async as response; else skeleton" style="position: relative;">
|
||||||
<form [formGroup]="channelStatusForm" class="formRadioGroup">
|
<form [formGroup]="channelStatusForm" class="formRadioGroup">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="channelStatusForm.get('status').value === 'open'">
|
||||||
<input ngbButton type="radio" [value]="'open'" fragment="open"><span i18n="open">Open</span>
|
<input type="radio" [value]="'open'" fragment="open" formControlName="status"><span i18n="open">Open</span>
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="channelStatusForm.get('status').value === 'closed'">
|
||||||
<input ngbButton type="radio" [value]="'closed'" fragment="closed"><span i18n="closed">Closed</span>
|
<input type="radio" [value]="'closed'" fragment="closed" formControlName="status"><span i18n="closed">Closed</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { BehaviorSubject, merge, Observable } from 'rxjs';
|
import { BehaviorSubject, merge, Observable } from 'rxjs';
|
||||||
import { map, switchMap, tap } from 'rxjs/operators';
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { isMobile } from '../../shared/common.utils';
|
import { isMobile } from '../../shared/common.utils';
|
||||||
@ -23,7 +23,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
itemsPerPage = 10;
|
itemsPerPage = 10;
|
||||||
page = 1;
|
page = 1;
|
||||||
channelsPage$ = new BehaviorSubject<number>(1);
|
channelsPage$ = new BehaviorSubject<number>(1);
|
||||||
channelStatusForm: FormGroup;
|
channelStatusForm: UntypedFormGroup;
|
||||||
defaultStatus = 'open';
|
defaultStatus = 'open';
|
||||||
status = 'open';
|
status = 'open';
|
||||||
publicKeySize = 25;
|
publicKeySize = 25;
|
||||||
@ -31,7 +31,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
) {
|
) {
|
||||||
this.channelStatusForm = this.formBuilder.group({
|
this.channelStatusForm = this.formBuilder.group({
|
||||||
status: [this.defaultStatus],
|
status: [this.defaultStatus],
|
||||||
|
@ -55,12 +55,12 @@
|
|||||||
|
|
||||||
<div class="toggle-holder">
|
<div class="toggle-holder">
|
||||||
<form [formGroup]="socketToggleForm" class="formRadioGroup">
|
<form [formGroup]="socketToggleForm" class="formRadioGroup">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="socket">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="socketToggleForm.get('socket').value === 0">
|
||||||
<input ngbButton type="radio" [value]="0">IPv4
|
<input type="radio" [value]="0" formControlName="socket">IPv4
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="socketToggleForm.get('socket').value === 1">
|
||||||
<input ngbButton type="radio" [value]="1">IPv6
|
<input type="radio" [value]="1" formControlName="socket">IPv6
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { map, Observable, share } from 'rxjs';
|
import { map, Observable, share } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||||
@ -17,12 +17,12 @@ export class GroupComponent implements OnInit {
|
|||||||
skeletonLines: number[] = [];
|
skeletonLines: number[] = [];
|
||||||
selectedSocketIndex = 0;
|
selectedSocketIndex = 0;
|
||||||
qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
socketToggleForm: FormGroup;
|
socketToggleForm: UntypedFormGroup;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < 20; ++i) {
|
for (let i = 0; i < 20; ++i) {
|
||||||
this.skeletonLines.push(i);
|
this.skeletonLines.push(i);
|
||||||
|
@ -12,6 +12,7 @@ import { ChannelsListComponent } from './channels-list/channels-list.component';
|
|||||||
import { ChannelComponent } from './channel/channel.component';
|
import { ChannelComponent } from './channel/channel.component';
|
||||||
import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component';
|
import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component';
|
||||||
import { ChannelBoxComponent } from './channel/channel-box/channel-box.component';
|
import { ChannelBoxComponent } from './channel/channel-box/channel-box.component';
|
||||||
|
import { ChannelCloseBoxComponent } from './channel/channel-close-box/channel-close-box.component';
|
||||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
|
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
|
||||||
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
|
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
|
||||||
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
|
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
|
||||||
@ -45,6 +46,7 @@ import { GroupComponent } from './group/group.component';
|
|||||||
ChannelComponent,
|
ChannelComponent,
|
||||||
LightningWrapperComponent,
|
LightningWrapperComponent,
|
||||||
ChannelBoxComponent,
|
ChannelBoxComponent,
|
||||||
|
ChannelCloseBoxComponent,
|
||||||
ClosingTypeComponent,
|
ClosingTypeComponent,
|
||||||
LightningStatisticsChartComponent,
|
LightningStatisticsChartComponent,
|
||||||
NodesNetworksChartComponent,
|
NodesNetworksChartComponent,
|
||||||
@ -81,6 +83,7 @@ import { GroupComponent } from './group/group.component';
|
|||||||
ChannelComponent,
|
ChannelComponent,
|
||||||
LightningWrapperComponent,
|
LightningWrapperComponent,
|
||||||
ChannelBoxComponent,
|
ChannelBoxComponent,
|
||||||
|
ChannelCloseBoxComponent,
|
||||||
ClosingTypeComponent,
|
ClosingTypeComponent,
|
||||||
LightningStatisticsChartComponent,
|
LightningStatisticsChartComponent,
|
||||||
NodesNetworksChartComponent,
|
NodesNetworksChartComponent,
|
||||||
|
@ -3,7 +3,7 @@ import { EChartsOption } from 'echarts';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { switchMap, tap } from 'rxjs/operators';
|
import { switchMap, tap } from 'rxjs/operators';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { download } from '../../shared/graphs.utils';
|
import { download } from '../../shared/graphs.utils';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
@ -29,7 +29,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
|||||||
@Input() widget = false;
|
@Input() widget = false;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
|
@ -9,27 +9,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(nodesNetworkObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(nodesNetworkObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 30">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 30" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 1M
|
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 90">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 90" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 3M
|
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 180">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 180" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 6M
|
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 365">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 365" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 1Y
|
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 730">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 730" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 2Y
|
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 1095">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 1095" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 3Y
|
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> ALL
|
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -3,7 +3,7 @@ import { EChartsOption, graphic, LineSeriesOption} from 'echarts';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { download } from '../../shared/graphs.utils';
|
import { download } from '../../shared/graphs.utils';
|
||||||
@ -32,7 +32,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
@Input() widget = false;
|
@Input() widget = false;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -51,7 +51,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private amountShortenerPipe: AmountShortenerPipe,
|
private amountShortenerPipe: AmountShortenerPipe,
|
||||||
|
@ -9,34 +9,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(capacityObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(capacityObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 30">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 30" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
|
||||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"
|
<input type="radio" [value]="'1m'" fragment="1m"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 1M
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 1M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 90">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 90" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"
|
<input type="radio" [value]="'3m'" fragment="3m"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 3M
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 180">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 180" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"
|
<input type="radio" [value]="'6m'" fragment="6m"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 6M
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 365">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 365" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
|
||||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"
|
<input type="radio" [value]="'1y'" fragment="1y"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 1Y
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 1Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 730">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 730" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
|
||||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"
|
<input type="radio" [value]="'2y'" fragment="2y"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 2Y
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 2Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 1095">
|
<label class="btn btn-primary btn-sm" *ngIf="stats.days >= 1095" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"
|
<input type="radio" [value]="'3y'" fragment="3y"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 3Y
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
<input ngbButton type="radio" [value]="'all'" fragment="all"
|
<input type="radio" [value]="'all'" fragment="all"
|
||||||
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> ALL
|
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> ALL
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { download } from '../../shared/graphs.utils';
|
import { download } from '../../shared/graphs.utils';
|
||||||
@ -31,7 +31,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
@Input() widget = false;
|
@Input() widget = false;
|
||||||
|
|
||||||
miningWindowPreference: string;
|
miningWindowPreference: string;
|
||||||
radioGroupForm: FormGroup;
|
radioGroupForm: UntypedFormGroup;
|
||||||
|
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
@ -50,7 +50,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
private amountShortenerPipe: AmountShortenerPipe,
|
private amountShortenerPipe: AmountShortenerPipe,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user