Merge branch 'master' into node_v20_to_matrix
This commit is contained in:
commit
69c54d7207
@ -29,7 +29,9 @@
|
|||||||
"ADVANCED_GBT_MEMPOOL": false,
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@ -123,5 +125,16 @@
|
|||||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||||
"BISQ_URL": "https://bisq.markets/api",
|
"BISQ_URL": "https://bisq.markets/api",
|
||||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"AUDIT": false,
|
||||||
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"SERVERS": [
|
||||||
|
"list",
|
||||||
|
"of",
|
||||||
|
"trusted",
|
||||||
|
"servers"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
backend/package-lock.json
generated
6
backend/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.21.3",
|
"@babel/core": "^7.21.3",
|
||||||
@ -7570,7 +7570,7 @@
|
|||||||
"name": "gbt",
|
"name": "gbt",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/cli": "^2.16.1"
|
"@napi-rs/cli": "^2.16.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
|
4
backend/rust-gbt/package-lock.json
generated
4
backend/rust-gbt/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gbt",
|
"name": "gbt",
|
||||||
"version": "0.1.0",
|
"version": "3.0.0-dev",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gbt",
|
"name": "gbt",
|
||||||
"version": "0.1.0",
|
"version": "3.0.0-dev",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/cli": "^2.16.1"
|
"@napi-rs/cli": "^2.16.1"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gbt",
|
"name": "gbt",
|
||||||
"version": "0.1.0",
|
"version": "3.0.0-dev",
|
||||||
"description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
|
"description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
|
@ -30,7 +30,9 @@
|
|||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"CPFP_INDEXING": true,
|
"CPFP_INDEXING": true,
|
||||||
"MAX_BLOCKS_BULK_QUERY": 999,
|
"MAX_BLOCKS_BULK_QUERY": 999,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 999
|
"DISK_CACHE_BLOCK_INTERVAL": 999,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
|
||||||
|
"ALLOW_UNREACHABLE": true
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
@ -119,5 +121,11 @@
|
|||||||
},
|
},
|
||||||
"CLIGHTNING": {
|
"CLIGHTNING": {
|
||||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"AUDIT": false,
|
||||||
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"SERVERS": []
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,6 +44,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
DISK_CACHE_BLOCK_INTERVAL: 6,
|
DISK_CACHE_BLOCK_INTERVAL: 6,
|
||||||
|
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
|
||||||
|
ALLOW_UNREACHABLE: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
||||||
@ -118,6 +120,13 @@ describe('Mempool Backend Config', () => {
|
|||||||
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||||
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.REPLICATION).toStrictEqual({
|
||||||
|
ENABLED: false,
|
||||||
|
AUDIT: false,
|
||||||
|
AUDIT_START_HEIGHT: 774000,
|
||||||
|
SERVERS: []
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
||||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
|
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
|
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
|
||||||
@ -14,7 +14,7 @@ class Audit {
|
|||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
|
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
@ -36,10 +36,13 @@ class Audit {
|
|||||||
// look for transactions that were expected in the template, but missing from the mined block
|
// look for transactions that were expected in the template, but missing from the mined block
|
||||||
for (const txid of projectedBlocks[0].transactionIds) {
|
for (const txid of projectedBlocks[0].transactionIds) {
|
||||||
if (!inBlock[txid]) {
|
if (!inBlock[txid]) {
|
||||||
// tx is recent, may have reached the miner too late for inclusion
|
|
||||||
if (rbfCache.isFullRbf(txid)) {
|
if (rbfCache.isFullRbf(txid)) {
|
||||||
fullrbf.push(txid);
|
fullrbf.push(txid);
|
||||||
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||||
|
// tx is recent, may have reached the miner too late for inclusion
|
||||||
|
fresh.push(txid);
|
||||||
|
} else if (mempool[txid]?.lastBoosted != null && (now - (mempool[txid]?.lastBoosted || 0)) <= PROPAGATION_MARGIN) {
|
||||||
|
// tx was recently cpfp'd, miner may not have the latest effective rate
|
||||||
fresh.push(txid);
|
fresh.push(txid);
|
||||||
} else {
|
} else {
|
||||||
isCensored[txid] = true;
|
isCensored[txid] = true;
|
||||||
|
@ -65,17 +65,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getBlockCount();
|
||||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
|
||||||
return result.find(tip => tip.status === 'active')!.height;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHashTip(): Promise<string> {
|
$getBlockHashTip(): Promise<string> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getBestBlockHash();
|
||||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
|
||||||
return result.find(tip => tip.status === 'active')!.hash;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
|
@ -121,7 +121,6 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', this.getAddressTransactions)
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@ -546,27 +545,28 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactions(req: Request, res: Response) {
|
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
let lastTxId: string = '';
|
||||||
|
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||||
|
lastTxId = req.query.after_txid;
|
||||||
|
}
|
||||||
|
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, lastTxId);
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAdressTxChain(req: Request, res: Response) {
|
|
||||||
res.status(501).send('Not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAddressPrefix(req: Request, res: Response) {
|
private async getAddressPrefix(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
@ -723,12 +723,7 @@ class BitcoinRoutes {
|
|||||||
private async $postTransaction(req: Request, res: Response) {
|
private async $postTransaction(req: Request, res: Response) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
try {
|
try {
|
||||||
let rawTx;
|
const rawTx = Common.getTransactionFromRequest(req, false);
|
||||||
if (typeof req.body === 'object') {
|
|
||||||
rawTx = Object.keys(req.body)[0];
|
|
||||||
} else {
|
|
||||||
rawTx = req.body;
|
|
||||||
}
|
|
||||||
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -739,12 +734,8 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
private async $postTransactionForm(req: Request, res: Response) {
|
private async $postTransactionForm(req: Request, res: Response) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
const matches = /tx=([a-z0-9]+)/.exec(req.body);
|
|
||||||
let txHex = '';
|
|
||||||
if (matches && matches[1]) {
|
|
||||||
txHex = matches[1];
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
const txHex = Common.getTransactionFromRequest(req, true);
|
||||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||||
res.send(txIdResult);
|
res.send(txIdResult);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -25,6 +25,7 @@ import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmen
|
|||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import chainTips from './chain-tips';
|
import chainTips from './chain-tips';
|
||||||
|
import websocketHandler from './websocket-handler';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -75,11 +76,14 @@ class Blocks {
|
|||||||
blockHash: string,
|
blockHash: string,
|
||||||
blockHeight: number,
|
blockHeight: number,
|
||||||
onlyCoinbase: boolean,
|
onlyCoinbase: boolean,
|
||||||
|
txIds: string[] | null = null,
|
||||||
quiet: boolean = false,
|
quiet: boolean = false,
|
||||||
addMempoolData: boolean = false,
|
addMempoolData: boolean = false,
|
||||||
): Promise<TransactionExtended[]> {
|
): Promise<TransactionExtended[]> {
|
||||||
const transactions: TransactionExtended[] = [];
|
const transactions: TransactionExtended[] = [];
|
||||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
if (!txIds) {
|
||||||
|
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
|
}
|
||||||
|
|
||||||
const mempool = memPool.getMempool();
|
const mempool = memPool.getMempool();
|
||||||
let transactionsFound = 0;
|
let transactionsFound = 0;
|
||||||
@ -553,7 +557,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
|
||||||
newlyIndexed++;
|
newlyIndexed++;
|
||||||
@ -585,7 +589,7 @@ class Blocks {
|
|||||||
|
|
||||||
let fastForwarded = false;
|
let fastForwarded = false;
|
||||||
let handledBlocks = 0;
|
let handledBlocks = 0;
|
||||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
const blockHeightTip = await bitcoinCoreApi.$getBlockHeightTip();
|
||||||
this.updateTimerProgress(timer, 'got block height tip');
|
this.updateTimerProgress(timer, 'got block height tip');
|
||||||
|
|
||||||
if (this.blocks.length === 0) {
|
if (this.blocks.length === 0) {
|
||||||
@ -638,11 +642,11 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
const blockHash = await bitcoinCoreApi.$getBlockHash(this.currentBlockHeight);
|
||||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, false, true) as MempoolTransactionExtended[];
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
// fill in missing transaction fee data from verboseBlock
|
// fill in missing transaction fee data from verboseBlock
|
||||||
for (let i = 0; i < transactions.length; i++) {
|
for (let i = 0; i < transactions.length; i++) {
|
||||||
@ -686,6 +690,8 @@ class Blocks {
|
|||||||
this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
|
this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
|
||||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
||||||
indexer.reindex();
|
indexer.reindex();
|
||||||
|
|
||||||
|
websocketHandler.handleReorg();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
import { Request } from 'express';
|
||||||
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
@ -86,19 +88,19 @@ export class Common {
|
|||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||||
if (match && match.txid !== tx.txid) {
|
if (match && match.txid !== tx.txid) {
|
||||||
replaced.add(match);
|
replaced.add(match);
|
||||||
|
// remove this tx from the spendMap
|
||||||
|
// prevents the same tx being replaced more than once
|
||||||
|
for (const replacedVin of match.vin) {
|
||||||
|
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||||
|
spendMap.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const key = `${vin.txid}:${vin.vout}`;
|
||||||
|
spendMap.delete(key);
|
||||||
|
}
|
||||||
if (replaced.size) {
|
if (replaced.size) {
|
||||||
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
|
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
|
||||||
}
|
}
|
||||||
// remove this tx from the spendMap
|
|
||||||
// prevents the same tx being replaced more than once
|
|
||||||
for (const vin of tx.vin) {
|
|
||||||
const key = `${vin.txid}:${vin.vout}`;
|
|
||||||
if (spendMap.get(key)?.txid === tx.txid) {
|
|
||||||
spendMap.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
@ -511,6 +513,115 @@ export class Common {
|
|||||||
static getNthPercentile(n: number, sortedDistribution: any[]): any {
|
static getNthPercentile(n: number, sortedDistribution: any[]): any {
|
||||||
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
|
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTransactionFromRequest(req: Request, form: boolean): string {
|
||||||
|
let rawTx: any = typeof req.body === 'object' && form
|
||||||
|
? Object.values(req.body)[0] as any
|
||||||
|
: req.body;
|
||||||
|
if (typeof rawTx !== 'string') {
|
||||||
|
throw Object.assign(new Error('Non-string request body'), { code: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support both upper and lower case hex
|
||||||
|
// Support both txHash= Form and direct API POST
|
||||||
|
const reg = form ? /^txHash=((?:[a-fA-F0-9]{2})+)$/ : /^((?:[a-fA-F0-9]{2})+)$/;
|
||||||
|
const matches = reg.exec(rawTx);
|
||||||
|
if (!matches || !matches[1]) {
|
||||||
|
throw Object.assign(new Error('Non-hex request body'), { code: -2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to be a hex string of multiple of 2
|
||||||
|
// Guaranteed to be lower case
|
||||||
|
// Guaranteed to pass validation (see function below)
|
||||||
|
return this.validateTransactionHex(matches[1].toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static validateTransactionHex(txhex: string): string {
|
||||||
|
// Do not mutate txhex
|
||||||
|
|
||||||
|
// We assume txhex to be valid hex (output of getTransactionFromRequest above)
|
||||||
|
|
||||||
|
// Check 1: Valid transaction parse
|
||||||
|
let tx: bitcoinjs.Transaction;
|
||||||
|
try {
|
||||||
|
tx = bitcoinjs.Transaction.fromHex(txhex);
|
||||||
|
} catch(e) {
|
||||||
|
throw Object.assign(new Error('Invalid transaction (could not parse)'), { code: -4 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Simple size check
|
||||||
|
if (tx.weight() > config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT) {
|
||||||
|
throw Object.assign(new Error(`Transaction too large (max ${config.MEMPOOL.MAX_PUSH_TX_SIZE_WEIGHT} weight units)`), { code: -3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3: Check unreachable script in taproot (if not allowed)
|
||||||
|
if (!config.MEMPOOL.ALLOW_UNREACHABLE) {
|
||||||
|
tx.ins.forEach(input => {
|
||||||
|
const witness = input.witness;
|
||||||
|
// See BIP 341: Script validation rules
|
||||||
|
const hasAnnex = witness.length >= 2 &&
|
||||||
|
witness[witness.length - 1][0] === 0x50;
|
||||||
|
const scriptSpendMinLength = hasAnnex ? 3 : 2;
|
||||||
|
const maybeScriptSpend = witness.length >= scriptSpendMinLength;
|
||||||
|
|
||||||
|
if (maybeScriptSpend) {
|
||||||
|
const controlBlock = witness[witness.length - scriptSpendMinLength + 1];
|
||||||
|
if (controlBlock.length === 0 || !this.isValidLeafVersion(controlBlock[0])) {
|
||||||
|
// Skip this input, it's not taproot
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Definitely taproot. Get script
|
||||||
|
const script = witness[witness.length - scriptSpendMinLength];
|
||||||
|
const decompiled = bitcoinjs.script.decompile(script);
|
||||||
|
if (!decompiled || decompiled.length < 2) {
|
||||||
|
// Skip this input
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Iterate up to second last (will look ahead 1 item)
|
||||||
|
for (let i = 0; i < decompiled.length - 1; i++) {
|
||||||
|
const first = decompiled[i];
|
||||||
|
const second = decompiled[i + 1];
|
||||||
|
if (
|
||||||
|
first === bitcoinjs.opcodes.OP_FALSE &&
|
||||||
|
second === bitcoinjs.opcodes.OP_IF
|
||||||
|
) {
|
||||||
|
throw Object.assign(new Error('Unreachable taproot scripts not allowed'), { code: -5 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through the input string untouched
|
||||||
|
return txhex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isValidLeafVersion(leafVersion: number): boolean {
|
||||||
|
// See Note 7 in BIP341
|
||||||
|
// https://github.com/bitcoin/bips/blob/66a1a8151021913047934ebab3f8883f2f8ca75b/bip-0341.mediawiki#cite_note-7
|
||||||
|
// "What constraints are there on the leaf version?"
|
||||||
|
|
||||||
|
// Must be an integer between 0 and 255
|
||||||
|
// Since we're parsing a byte
|
||||||
|
if (Math.floor(leafVersion) !== leafVersion || leafVersion < 0 || leafVersion > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// "the leaf version cannot be odd"
|
||||||
|
if ((leafVersion & 0x01) === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// "The values that comply to this rule are
|
||||||
|
// the 32 even values between 0xc0 and 0xfe
|
||||||
|
if (leafVersion >= 0xc0 && leafVersion <= 0xfe) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// and also 0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe."
|
||||||
|
if ([0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe].includes(leafVersion)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, invalid
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 63;
|
private static currentVersion = 64;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -543,6 +543,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
|
||||||
await this.updateToSchemaVersion(63);
|
await this.updateToSchemaVersion(63);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 64 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
|
||||||
|
await this.updateToSchemaVersion(64);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,7 @@ import DB from '../../database';
|
|||||||
import { ResultSetHeader } from 'mysql2';
|
import { ResultSetHeader } from 'mysql2';
|
||||||
import { ILightningApi } from '../lightning/lightning-api.interface';
|
import { ILightningApi } from '../lightning/lightning-api.interface';
|
||||||
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
|
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
|
||||||
|
import { bin2hex } from '../../utils/format';
|
||||||
|
|
||||||
class NodesApi {
|
class NodesApi {
|
||||||
public async $getWorldNodes(): Promise<any> {
|
public async $getWorldNodes(): Promise<any> {
|
||||||
@ -56,7 +57,8 @@ class NodesApi {
|
|||||||
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
|
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
|
||||||
as_number, city_id, country_id, subdivision_id, longitude, latitude,
|
as_number, city_id, country_id, subdivision_id, longitude, latitude,
|
||||||
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
|
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
|
||||||
geo_names_country.names as country, geo_names_subdivision.names as subdivision
|
geo_names_country.names as country, geo_names_subdivision.names as subdivision,
|
||||||
|
features
|
||||||
FROM nodes
|
FROM nodes
|
||||||
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
||||||
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
||||||
@ -76,6 +78,23 @@ class NodesApi {
|
|||||||
node.city = JSON.parse(node.city);
|
node.city = JSON.parse(node.city);
|
||||||
node.country = JSON.parse(node.country);
|
node.country = JSON.parse(node.country);
|
||||||
|
|
||||||
|
// Features
|
||||||
|
node.features = JSON.parse(node.features);
|
||||||
|
node.featuresBits = null;
|
||||||
|
if (node.features) {
|
||||||
|
let maxBit = 0;
|
||||||
|
for (const feature of node.features) {
|
||||||
|
maxBit = Math.max(maxBit, feature.bit);
|
||||||
|
}
|
||||||
|
maxBit = Math.ceil(maxBit / 4) * 4 - 1;
|
||||||
|
|
||||||
|
node.featuresBits = new Array(maxBit + 1).fill(0);
|
||||||
|
for (const feature of node.features) {
|
||||||
|
node.featuresBits[feature.bit] = 1;
|
||||||
|
}
|
||||||
|
node.featuresBits = bin2hex(node.featuresBits.reverse().join(''));
|
||||||
|
}
|
||||||
|
|
||||||
// Active channels and capacity
|
// Active channels and capacity
|
||||||
const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
|
const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
|
||||||
node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
|
node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
|
||||||
@ -656,10 +675,19 @@ class NodesApi {
|
|||||||
alias_search,
|
alias_search,
|
||||||
color,
|
color,
|
||||||
sockets,
|
sockets,
|
||||||
status
|
status,
|
||||||
|
features
|
||||||
)
|
)
|
||||||
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1)
|
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1, ?)
|
||||||
ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, alias_search = ?, color = ?, sockets = ?, status = 1`;
|
ON DUPLICATE KEY UPDATE
|
||||||
|
updated_at = FROM_UNIXTIME(?),
|
||||||
|
alias = ?,
|
||||||
|
alias_search = ?,
|
||||||
|
color = ?,
|
||||||
|
sockets = ?,
|
||||||
|
status = 1,
|
||||||
|
features = ?
|
||||||
|
`;
|
||||||
|
|
||||||
await DB.query(query, [
|
await DB.query(query, [
|
||||||
node.pub_key,
|
node.pub_key,
|
||||||
@ -668,11 +696,13 @@ class NodesApi {
|
|||||||
this.aliasToSearchText(node.alias),
|
this.aliasToSearchText(node.alias),
|
||||||
node.color,
|
node.color,
|
||||||
sockets,
|
sockets,
|
||||||
|
JSON.stringify(node.features),
|
||||||
node.last_update,
|
node.last_update,
|
||||||
node.alias,
|
node.alias,
|
||||||
this.aliasToSearchText(node.alias),
|
this.aliasToSearchText(node.alias),
|
||||||
node.color,
|
node.color,
|
||||||
sockets,
|
sockets,
|
||||||
|
JSON.stringify(node.features),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
@ -2,8 +2,91 @@ import { ILightningApi } from '../lightning-api.interface';
|
|||||||
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { Common } from '../../common';
|
import { Common } from '../../common';
|
||||||
|
import { hex2bin } from '../../../utils/format';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
|
||||||
|
// https://github.com/lightningnetwork/lnd/blob/master/lnwire/features.go
|
||||||
|
export enum FeatureBits {
|
||||||
|
DataLossProtectRequired = 0,
|
||||||
|
DataLossProtectOptional = 1,
|
||||||
|
InitialRoutingSync = 3,
|
||||||
|
UpfrontShutdownScriptRequired = 4,
|
||||||
|
UpfrontShutdownScriptOptional = 5,
|
||||||
|
GossipQueriesRequired = 6,
|
||||||
|
GossipQueriesOptional = 7,
|
||||||
|
TLVOnionPayloadRequired = 8,
|
||||||
|
TLVOnionPayloadOptional = 9,
|
||||||
|
StaticRemoteKeyRequired = 12,
|
||||||
|
StaticRemoteKeyOptional = 13,
|
||||||
|
PaymentAddrRequired = 14,
|
||||||
|
PaymentAddrOptional = 15,
|
||||||
|
MPPRequired = 16,
|
||||||
|
MPPOptional = 17,
|
||||||
|
WumboChannelsRequired = 18,
|
||||||
|
WumboChannelsOptional = 19,
|
||||||
|
AnchorsRequired = 20,
|
||||||
|
AnchorsOptional = 21,
|
||||||
|
AnchorsZeroFeeHtlcTxRequired = 22,
|
||||||
|
AnchorsZeroFeeHtlcTxOptional = 23,
|
||||||
|
ShutdownAnySegwitRequired = 26,
|
||||||
|
ShutdownAnySegwitOptional = 27,
|
||||||
|
AMPRequired = 30,
|
||||||
|
AMPOptional = 31,
|
||||||
|
ExplicitChannelTypeRequired = 44,
|
||||||
|
ExplicitChannelTypeOptional = 45,
|
||||||
|
ScidAliasRequired = 46,
|
||||||
|
ScidAliasOptional = 47,
|
||||||
|
PaymentMetadataRequired = 48,
|
||||||
|
PaymentMetadataOptional = 49,
|
||||||
|
ZeroConfRequired = 50,
|
||||||
|
ZeroConfOptional = 51,
|
||||||
|
KeysendRequired = 54,
|
||||||
|
KeysendOptional = 55,
|
||||||
|
ScriptEnforcedLeaseRequired = 2022,
|
||||||
|
ScriptEnforcedLeaseOptional = 2023,
|
||||||
|
MaxBolt11Feature = 5114,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeaturesMap = new Map<FeatureBits, string>([
|
||||||
|
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
|
||||||
|
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
|
||||||
|
[FeatureBits.InitialRoutingSync, 'initial-routing-sync'],
|
||||||
|
[FeatureBits.UpfrontShutdownScriptRequired, 'upfront-shutdown-script'],
|
||||||
|
[FeatureBits.UpfrontShutdownScriptOptional, 'upfront-shutdown-script'],
|
||||||
|
[FeatureBits.GossipQueriesRequired, 'gossip-queries'],
|
||||||
|
[FeatureBits.GossipQueriesOptional, 'gossip-queries'],
|
||||||
|
[FeatureBits.TLVOnionPayloadRequired, 'tlv-onion'],
|
||||||
|
[FeatureBits.TLVOnionPayloadOptional, 'tlv-onion'],
|
||||||
|
[FeatureBits.StaticRemoteKeyOptional, 'static-remote-key'],
|
||||||
|
[FeatureBits.StaticRemoteKeyRequired, 'static-remote-key'],
|
||||||
|
[FeatureBits.PaymentAddrOptional, 'payment-addr'],
|
||||||
|
[FeatureBits.PaymentAddrRequired, 'payment-addr'],
|
||||||
|
[FeatureBits.MPPOptional, 'multi-path-payments'],
|
||||||
|
[FeatureBits.MPPRequired, 'multi-path-payments'],
|
||||||
|
[FeatureBits.AnchorsRequired, 'anchor-commitments'],
|
||||||
|
[FeatureBits.AnchorsOptional, 'anchor-commitments'],
|
||||||
|
[FeatureBits.AnchorsZeroFeeHtlcTxRequired, 'anchors-zero-fee-htlc-tx'],
|
||||||
|
[FeatureBits.AnchorsZeroFeeHtlcTxOptional, 'anchors-zero-fee-htlc-tx'],
|
||||||
|
[FeatureBits.WumboChannelsRequired, 'wumbo-channels'],
|
||||||
|
[FeatureBits.WumboChannelsOptional, 'wumbo-channels'],
|
||||||
|
[FeatureBits.AMPRequired, 'amp'],
|
||||||
|
[FeatureBits.AMPOptional, 'amp'],
|
||||||
|
[FeatureBits.PaymentMetadataOptional, 'payment-metadata'],
|
||||||
|
[FeatureBits.PaymentMetadataRequired, 'payment-metadata'],
|
||||||
|
[FeatureBits.ExplicitChannelTypeOptional, 'explicit-commitment-type'],
|
||||||
|
[FeatureBits.ExplicitChannelTypeRequired, 'explicit-commitment-type'],
|
||||||
|
[FeatureBits.KeysendOptional, 'keysend'],
|
||||||
|
[FeatureBits.KeysendRequired, 'keysend'],
|
||||||
|
[FeatureBits.ScriptEnforcedLeaseRequired, 'script-enforced-lease'],
|
||||||
|
[FeatureBits.ScriptEnforcedLeaseOptional, 'script-enforced-lease'],
|
||||||
|
[FeatureBits.ScidAliasRequired, 'scid-alias'],
|
||||||
|
[FeatureBits.ScidAliasOptional, 'scid-alias'],
|
||||||
|
[FeatureBits.ZeroConfRequired, 'zero-conf'],
|
||||||
|
[FeatureBits.ZeroConfOptional, 'zero-conf'],
|
||||||
|
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
|
||||||
|
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a clightning "listnode" entry to a lnd node entry
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
*/
|
*/
|
||||||
@ -17,10 +100,36 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
|||||||
custom_records = undefined;
|
custom_records = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodeFeatures: ILightningApi.Feature[] = [];
|
||||||
|
const nodeFeaturesBinary = hex2bin(clNode.features).split('').reverse().join('');
|
||||||
|
|
||||||
|
for (let i = 0; i < nodeFeaturesBinary.length; i++) {
|
||||||
|
if (nodeFeaturesBinary[i] === '0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const feature = FeaturesMap.get(i);
|
||||||
|
if (!feature) {
|
||||||
|
nodeFeatures.push({
|
||||||
|
bit: i,
|
||||||
|
name: 'unknown',
|
||||||
|
is_required: i % 2 === 0,
|
||||||
|
is_known: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nodeFeatures.push({
|
||||||
|
bit: i,
|
||||||
|
name: feature,
|
||||||
|
is_required: i % 2 === 0,
|
||||||
|
is_known: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias: clNode.alias ?? '',
|
alias: clNode.alias ?? '',
|
||||||
color: `#${clNode.color ?? ''}`,
|
color: `#${clNode.color ?? ''}`,
|
||||||
features: [], // TODO parse and return clNode.feature
|
features: nodeFeatures,
|
||||||
pub_key: clNode.nodeid,
|
pub_key: clNode.nodeid,
|
||||||
addresses: clNode.addresses?.map((addr) => {
|
addresses: clNode.addresses?.map((addr) => {
|
||||||
let address = addr.address;
|
let address = addr.address;
|
||||||
|
@ -79,6 +79,7 @@ export namespace ILightningApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Feature {
|
export interface Feature {
|
||||||
|
bit: number;
|
||||||
name: string;
|
name: string;
|
||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
is_known: boolean;
|
is_known: boolean;
|
||||||
|
@ -41,8 +41,23 @@ class LndApi implements AbstractLightningApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
||||||
return axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
|
const graph = await axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
const nodeFeatures: ILightningApi.Feature[] = [];
|
||||||
|
for (const bit in node.features) {
|
||||||
|
nodeFeatures.push({
|
||||||
|
bit: parseInt(bit, 10),
|
||||||
|
name: node.features[bit].name,
|
||||||
|
is_required: node.features[bit].is_required,
|
||||||
|
is_known: node.features[bit].is_known,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
node.features = nodeFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,6 +457,7 @@ class MempoolBlocks {
|
|||||||
};
|
};
|
||||||
if (matched) {
|
if (matched) {
|
||||||
descendants.push(relative);
|
descendants.push(relative);
|
||||||
|
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
||||||
} else {
|
} else {
|
||||||
ancestors.push(relative);
|
ancestors.push(relative);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { Common } from "./common";
|
|||||||
interface RbfTransaction extends TransactionStripped {
|
interface RbfTransaction extends TransactionStripped {
|
||||||
rbf?: boolean;
|
rbf?: boolean;
|
||||||
mined?: boolean;
|
mined?: boolean;
|
||||||
|
fullRbf?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbfTree {
|
interface RbfTree {
|
||||||
@ -17,6 +18,16 @@ interface RbfTree {
|
|||||||
replaces: RbfTree[];
|
replaces: RbfTree[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReplacementInfo {
|
||||||
|
mined: boolean;
|
||||||
|
fullRbf: boolean;
|
||||||
|
txid: string;
|
||||||
|
oldFee: number;
|
||||||
|
oldVsize: number;
|
||||||
|
newFee: number;
|
||||||
|
newVsize: number;
|
||||||
|
}
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private replacedBy: Map<string, string> = new Map();
|
private replacedBy: Map<string, string> = new Map();
|
||||||
private replaces: Map<string, string[]> = new Map();
|
private replaces: Map<string, string[]> = new Map();
|
||||||
@ -41,11 +52,15 @@ class RbfCache {
|
|||||||
this.txs.set(newTx.txid, newTxExtended);
|
this.txs.set(newTx.txid, newTxExtended);
|
||||||
|
|
||||||
// maintain rbf trees
|
// maintain rbf trees
|
||||||
let fullRbf = false;
|
let txFullRbf = false;
|
||||||
|
let treeFullRbf = false;
|
||||||
const replacedTrees: RbfTree[] = [];
|
const replacedTrees: RbfTree[] = [];
|
||||||
for (const replacedTxExtended of replaced) {
|
for (const replacedTxExtended of replaced) {
|
||||||
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
||||||
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||||
|
if (!replacedTx.rbf) {
|
||||||
|
txFullRbf = true;
|
||||||
|
}
|
||||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||||
if (this.treeMap.has(replacedTx.txid)) {
|
if (this.treeMap.has(replacedTx.txid)) {
|
||||||
const treeId = this.treeMap.get(replacedTx.txid);
|
const treeId = this.treeMap.get(replacedTx.txid);
|
||||||
@ -55,7 +70,7 @@ class RbfCache {
|
|||||||
if (tree) {
|
if (tree) {
|
||||||
tree.interval = newTime - tree?.time;
|
tree.interval = newTime - tree?.time;
|
||||||
replacedTrees.push(tree);
|
replacedTrees.push(tree);
|
||||||
fullRbf = fullRbf || tree.fullRbf;
|
treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -67,15 +82,16 @@ class RbfCache {
|
|||||||
fullRbf: !replacedTx.rbf,
|
fullRbf: !replacedTx.rbf,
|
||||||
replaces: [],
|
replaces: [],
|
||||||
});
|
});
|
||||||
fullRbf = fullRbf || !replacedTx.rbf;
|
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
||||||
this.txs.set(replacedTx.txid, replacedTxExtended);
|
this.txs.set(replacedTx.txid, replacedTxExtended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
newTx.fullRbf = txFullRbf;
|
||||||
const treeId = replacedTrees[0].tx.txid;
|
const treeId = replacedTrees[0].tx.txid;
|
||||||
const newTree = {
|
const newTree = {
|
||||||
tx: newTx,
|
tx: newTx,
|
||||||
time: newTime,
|
time: newTime,
|
||||||
fullRbf,
|
fullRbf: treeFullRbf,
|
||||||
replaces: replacedTrees
|
replaces: replacedTrees
|
||||||
};
|
};
|
||||||
this.rbfTrees.set(treeId, newTree);
|
this.rbfTrees.set(treeId, newTree);
|
||||||
@ -349,6 +365,27 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLatestRbfSummary(): ReplacementInfo[] {
|
||||||
|
const rbfList = this.getRbfTrees(false);
|
||||||
|
return rbfList.slice(0, 6).map(rbfTree => {
|
||||||
|
let oldFee = 0;
|
||||||
|
let oldVsize = 0;
|
||||||
|
for (const replaced of rbfTree.replaces) {
|
||||||
|
oldFee += replaced.tx.fee;
|
||||||
|
oldVsize += replaced.tx.vsize;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
txid: rbfTree.tx.txid,
|
||||||
|
mined: !!rbfTree.tx.mined,
|
||||||
|
fullRbf: !!rbfTree.tx.fullRbf,
|
||||||
|
oldFee,
|
||||||
|
oldVsize,
|
||||||
|
newFee: rbfTree.tx.fee,
|
||||||
|
newVsize: rbfTree.tx.vsize,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new RbfCache();
|
export default new RbfCache();
|
||||||
|
@ -12,7 +12,7 @@ import { Common } from './common';
|
|||||||
import loadingIndicators from './loading-indicators';
|
import loadingIndicators from './loading-indicators';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache, { ReplacementInfo } from './rbf-cache';
|
||||||
import difficultyAdjustment from './difficulty-adjustment';
|
import difficultyAdjustment from './difficulty-adjustment';
|
||||||
import feeApi from './fee-api';
|
import feeApi from './fee-api';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
@ -40,6 +40,7 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
private socketData: { [key: string]: string } = {};
|
private socketData: { [key: string]: string } = {};
|
||||||
private serializedInitData: string = '{}';
|
private serializedInitData: string = '{}';
|
||||||
|
private lastRbfSummary: ReplacementInfo | null = null;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -225,8 +226,19 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage && parsedMessage['track-rbf-summary'] != null) {
|
||||||
|
if (parsedMessage['track-rbf-summary']) {
|
||||||
|
client['track-rbf-summary'] = true;
|
||||||
|
if (this.socketData['rbfSummary'] != null) {
|
||||||
|
response['rbfLatestSummary'] = this.socketData['rbfSummary'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client['track-rbf-summary'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
if (!this.socketData['blocks']?.length || !this.socketData['da']) {
|
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
|
||||||
this.updateSocketData();
|
this.updateSocketData();
|
||||||
}
|
}
|
||||||
if (!this.socketData['blocks']?.length) {
|
if (!this.socketData['blocks']?.length) {
|
||||||
@ -333,6 +345,40 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleReorg(): void {
|
||||||
|
if (!this.wss) {
|
||||||
|
throw new Error('WebSocket.Server is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
|
|
||||||
|
// update init data
|
||||||
|
this.updateSocketDataFields({
|
||||||
|
'blocks': blocks.getBlocks(),
|
||||||
|
'da': da?.previousTime ? da : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.clients.forEach((client) => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {};
|
||||||
|
|
||||||
|
if (client['want-blocks']) {
|
||||||
|
response['blocks'] = this.socketData['blocks'];
|
||||||
|
}
|
||||||
|
if (client['want-stats']) {
|
||||||
|
response['da'] = this.socketData['da'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(response).length) {
|
||||||
|
const serializedResponse = this.serializeResponse(response);
|
||||||
|
client.send(serializedResponse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
@ -361,10 +407,13 @@ class WebsocketHandler {
|
|||||||
const rbfChanges = rbfCache.getRbfChanges();
|
const rbfChanges = rbfCache.getRbfChanges();
|
||||||
let rbfReplacements;
|
let rbfReplacements;
|
||||||
let fullRbfReplacements;
|
let fullRbfReplacements;
|
||||||
|
let rbfSummary;
|
||||||
if (Object.keys(rbfChanges.trees).length) {
|
if (Object.keys(rbfChanges.trees).length) {
|
||||||
rbfReplacements = rbfCache.getRbfTrees(false);
|
rbfReplacements = rbfCache.getRbfTrees(false);
|
||||||
fullRbfReplacements = rbfCache.getRbfTrees(true);
|
fullRbfReplacements = rbfCache.getRbfTrees(true);
|
||||||
|
rbfSummary = rbfCache.getLatestRbfSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const deletedTx of deletedTransactions) {
|
for (const deletedTx of deletedTransactions) {
|
||||||
rbfCache.evict(deletedTx.txid);
|
rbfCache.evict(deletedTx.txid);
|
||||||
}
|
}
|
||||||
@ -372,10 +421,10 @@ class WebsocketHandler {
|
|||||||
memPool.addToSpendMap(newTransactions);
|
memPool.addToSpendMap(newTransactions);
|
||||||
const recommendedFees = feeApi.getRecommendedFee();
|
const recommendedFees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
const latestTransactions = memPool.getLatestTransactions();
|
||||||
|
|
||||||
// update init data
|
// update init data
|
||||||
this.updateSocketDataFields({
|
const socketDataFields = {
|
||||||
'mempoolInfo': mempoolInfo,
|
'mempoolInfo': mempoolInfo,
|
||||||
'vBytesPerSecond': vBytesPerSecond,
|
'vBytesPerSecond': vBytesPerSecond,
|
||||||
'mempool-blocks': mBlocks,
|
'mempool-blocks': mBlocks,
|
||||||
@ -383,7 +432,11 @@ class WebsocketHandler {
|
|||||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||||
'da': da?.previousTime ? da : undefined,
|
'da': da?.previousTime ? da : undefined,
|
||||||
'fees': recommendedFees,
|
'fees': recommendedFees,
|
||||||
});
|
};
|
||||||
|
if (rbfSummary) {
|
||||||
|
socketDataFields['rbfSummary'] = rbfSummary;
|
||||||
|
}
|
||||||
|
this.updateSocketDataFields(socketDataFields);
|
||||||
|
|
||||||
// cache serialized objects to avoid stringify-ing the same thing for every client
|
// cache serialized objects to avoid stringify-ing the same thing for every client
|
||||||
const responseCache = { ...this.socketData };
|
const responseCache = { ...this.socketData };
|
||||||
@ -567,6 +620,10 @@ class WebsocketHandler {
|
|||||||
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
|
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client['track-rbf-summary'] && rbfSummary) {
|
||||||
|
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
const serializedResponse = this.serializeResponse(response);
|
const serializedResponse = this.serializeResponse(response);
|
||||||
client.send(serializedResponse);
|
client.send(serializedResponse);
|
||||||
|
@ -35,6 +35,8 @@ interface IConfig {
|
|||||||
CPFP_INDEXING: boolean;
|
CPFP_INDEXING: boolean;
|
||||||
MAX_BLOCKS_BULK_QUERY: number;
|
MAX_BLOCKS_BULK_QUERY: number;
|
||||||
DISK_CACHE_BLOCK_INTERVAL: number;
|
DISK_CACHE_BLOCK_INTERVAL: number;
|
||||||
|
MAX_PUSH_TX_SIZE_WEIGHT: number;
|
||||||
|
ALLOW_UNREACHABLE: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@ -130,6 +132,12 @@ interface IConfig {
|
|||||||
GEOLITE2_ASN: string;
|
GEOLITE2_ASN: string;
|
||||||
GEOIP2_ISP: string;
|
GEOIP2_ISP: string;
|
||||||
},
|
},
|
||||||
|
REPLICATION: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
AUDIT: boolean;
|
||||||
|
AUDIT_START_HEIGHT: number;
|
||||||
|
SERVERS: string[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
@ -165,6 +173,8 @@ const defaults: IConfig = {
|
|||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||||
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
||||||
|
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
|
||||||
|
'ALLOW_UNREACHABLE': true,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
@ -260,6 +270,12 @@ const defaults: IConfig = {
|
|||||||
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||||
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||||
},
|
},
|
||||||
|
'REPLICATION': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'AUDIT': false,
|
||||||
|
'AUDIT_START_HEIGHT': 774000,
|
||||||
|
'SERVERS': [],
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@ -279,6 +295,7 @@ class Config implements IConfig {
|
|||||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
MAXMIND: IConfig['MAXMIND'];
|
MAXMIND: IConfig['MAXMIND'];
|
||||||
|
REPLICATION: IConfig['REPLICATION'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@ -298,6 +315,7 @@ class Config implements IConfig {
|
|||||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
this.MAXMIND = configs.MAXMIND;
|
this.MAXMIND = configs.MAXMIND;
|
||||||
|
this.REPLICATION = configs.REPLICATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
@ -169,6 +169,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runMainUpdateLoop(): Promise<void> {
|
async runMainUpdateLoop(): Promise<void> {
|
||||||
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await memPool.$updateMemPoolInfo();
|
await memPool.$updateMemPoolInfo();
|
||||||
@ -188,7 +189,9 @@ class Server {
|
|||||||
indexer.$run();
|
indexer.$run();
|
||||||
|
|
||||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 1 : config.MEMPOOL.POLL_RATE_MS);
|
const elapsed = Date.now() - start;
|
||||||
|
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
|
||||||
|
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
|
||||||
this.backendRetryCount = 0;
|
this.backendRetryCount = 0;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.backendRetryCount++;
|
this.backendRetryCount++;
|
||||||
|
@ -6,6 +6,8 @@ import logger from './logger';
|
|||||||
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||||
import priceUpdater from './tasks/price-updater';
|
import priceUpdater from './tasks/price-updater';
|
||||||
import PricesRepository from './repositories/PricesRepository';
|
import PricesRepository from './repositories/PricesRepository';
|
||||||
|
import config from './config';
|
||||||
|
import auditReplicator from './replication/AuditReplication';
|
||||||
|
|
||||||
export interface CoreIndex {
|
export interface CoreIndex {
|
||||||
name: string;
|
name: string;
|
||||||
@ -72,7 +74,7 @@ class Indexer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) {
|
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
this.tasksRunning.push(task);
|
this.tasksRunning.push(task);
|
||||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||||
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||||
@ -135,6 +137,7 @@ class Indexer {
|
|||||||
await blocks.$generateBlocksSummariesDatabase();
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
await blocks.$generateCPFPDatabase();
|
await blocks.$generateCPFPDatabase();
|
||||||
await blocks.$generateAuditStats();
|
await blocks.$generateAuditStats();
|
||||||
|
await auditReplicator.$sync();
|
||||||
} 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));
|
||||||
|
@ -100,6 +100,7 @@ export interface MempoolTransactionExtended extends TransactionExtended {
|
|||||||
adjustedVsize: number;
|
adjustedVsize: number;
|
||||||
adjustedFeePerVsize: number;
|
adjustedFeePerVsize: number;
|
||||||
inputs?: number[];
|
inputs?: number[];
|
||||||
|
lastBoosted?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditTransaction {
|
export interface AuditTransaction {
|
||||||
@ -236,6 +237,15 @@ export interface BlockSummary {
|
|||||||
transactions: TransactionStripped[];
|
transactions: TransactionStripped[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditSummary extends BlockAudit {
|
||||||
|
timestamp?: number,
|
||||||
|
size?: number,
|
||||||
|
weight?: number,
|
||||||
|
tx_count?: number,
|
||||||
|
transactions: TransactionStripped[];
|
||||||
|
template?: TransactionStripped[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface BlockPrice {
|
export interface BlockPrice {
|
||||||
height: number;
|
height: number;
|
||||||
priceId: number;
|
priceId: number;
|
||||||
|
134
backend/src/replication/AuditReplication.ts
Normal file
134
backend/src/replication/AuditReplication.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { AuditSummary } from '../mempool.interfaces';
|
||||||
|
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import blocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import { $sync } from './replicator';
|
||||||
|
import config from '../config';
|
||||||
|
import { Common } from '../api/common';
|
||||||
|
import blocks from '../api/blocks';
|
||||||
|
|
||||||
|
const BATCH_SIZE = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs missing block template and audit data from trusted servers
|
||||||
|
*/
|
||||||
|
class AuditReplication {
|
||||||
|
inProgress: boolean = false;
|
||||||
|
skip: Set<string> = new Set();
|
||||||
|
|
||||||
|
public async $sync(): Promise<void> {
|
||||||
|
if (!config.REPLICATION.ENABLED || !config.REPLICATION.AUDIT) {
|
||||||
|
// replication not enabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.inProgress) {
|
||||||
|
logger.info(`AuditReplication sync already in progress`, 'Replication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
const missingAudits = await this.$getMissingAuditBlocks();
|
||||||
|
|
||||||
|
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||||
|
|
||||||
|
let totalSynced = 0;
|
||||||
|
let totalMissed = 0;
|
||||||
|
let loggerTimer = Date.now();
|
||||||
|
// process missing audits in batches of
|
||||||
|
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
||||||
|
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
||||||
|
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
||||||
|
const synced = results.reduce((total, status) => status ? total + 1 : total, 0);
|
||||||
|
totalSynced += synced;
|
||||||
|
totalMissed += (slice.length - synced);
|
||||||
|
if (Date.now() - loggerTimer > 10000) {
|
||||||
|
loggerTimer = Date.now();
|
||||||
|
logger.info(`Found ${totalSynced} / ${totalSynced + totalMissed} of ${missingAudits.length} missing audits`, 'Replication');
|
||||||
|
}
|
||||||
|
await Common.sleep$(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Fetched ${totalSynced} audits, ${totalMissed} still missing`, 'Replication');
|
||||||
|
|
||||||
|
this.inProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $syncAudit(hash: string): Promise<boolean> {
|
||||||
|
if (this.skip.has(hash)) {
|
||||||
|
// we already know none of our trusted servers have this audit
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
// start with a random server so load is uniformly spread
|
||||||
|
const syncResult = await $sync(`/api/v1/block/${hash}/audit-summary`);
|
||||||
|
if (syncResult) {
|
||||||
|
if (syncResult.data?.template?.length) {
|
||||||
|
await this.$saveAuditData(hash, syncResult.data);
|
||||||
|
logger.info(`Imported audit data from ${syncResult.server} for block ${syncResult.data.height} (${hash})`);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
if (!syncResult.data && !syncResult.exists) {
|
||||||
|
this.skip.add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getMissingAuditBlocks(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const startHeight = config.REPLICATION.AUDIT_START_HEIGHT || 0;
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT auditable.hash, auditable.height
|
||||||
|
FROM (
|
||||||
|
SELECT hash, height
|
||||||
|
FROM blocks
|
||||||
|
WHERE height >= ?
|
||||||
|
) AS auditable
|
||||||
|
LEFT JOIN blocks_audits ON auditable.hash = blocks_audits.hash
|
||||||
|
WHERE blocks_audits.hash IS NULL
|
||||||
|
ORDER BY auditable.height DESC
|
||||||
|
`, [startHeight]);
|
||||||
|
return rows.map(row => row.hash);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch missing audit blocks from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $saveAuditData(blockHash: string, auditSummary: AuditSummary): Promise<void> {
|
||||||
|
// save audit & template to DB
|
||||||
|
await blocksSummariesRepository.$saveTemplate({
|
||||||
|
height: auditSummary.height,
|
||||||
|
template: {
|
||||||
|
id: blockHash,
|
||||||
|
transactions: auditSummary.template || []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await blocksAuditsRepository.$saveAudit({
|
||||||
|
hash: blockHash,
|
||||||
|
height: auditSummary.height,
|
||||||
|
time: auditSummary.timestamp || auditSummary.time,
|
||||||
|
missingTxs: auditSummary.missingTxs || [],
|
||||||
|
addedTxs: auditSummary.addedTxs || [],
|
||||||
|
freshTxs: auditSummary.freshTxs || [],
|
||||||
|
sigopTxs: auditSummary.sigopTxs || [],
|
||||||
|
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
||||||
|
matchRate: auditSummary.matchRate,
|
||||||
|
expectedFees: auditSummary.expectedFees,
|
||||||
|
expectedWeight: auditSummary.expectedWeight,
|
||||||
|
});
|
||||||
|
// add missing data to cached blocks
|
||||||
|
const cachedBlock = blocks.getBlocks().find(block => block.id === blockHash);
|
||||||
|
if (cachedBlock) {
|
||||||
|
cachedBlock.extras.matchRate = auditSummary.matchRate;
|
||||||
|
cachedBlock.extras.expectedFees = auditSummary.expectedFees || null;
|
||||||
|
cachedBlock.extras.expectedWeight = auditSummary.expectedWeight || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuditReplication();
|
||||||
|
|
70
backend/src/replication/replicator.ts
Normal file
70
backend/src/replication/replicator.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import config from '../config';
|
||||||
|
import backendInfo from '../api/backend-info';
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
|
export async function $sync(path): Promise<{ data?: any, exists: boolean, server?: string }> {
|
||||||
|
// start with a random server so load is uniformly spread
|
||||||
|
let allMissing = true;
|
||||||
|
const offset = Math.floor(Math.random() * config.REPLICATION.SERVERS.length);
|
||||||
|
for (let i = 0; i < config.REPLICATION.SERVERS.length; i++) {
|
||||||
|
const server = config.REPLICATION.SERVERS[(i + offset) % config.REPLICATION.SERVERS.length];
|
||||||
|
// don't query ourself
|
||||||
|
if (server === backendInfo.getBackendInfo().hostname) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await query(`https://${server}${path}`);
|
||||||
|
if (result) {
|
||||||
|
return { data: result, exists: true, server };
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.response?.status === 404) {
|
||||||
|
// this server is also missing this data
|
||||||
|
} else {
|
||||||
|
// something else went wrong
|
||||||
|
allMissing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: !allMissing };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function query(path): Promise<object> {
|
||||||
|
type axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
|
httpsAgent?: https.Agent;
|
||||||
|
};
|
||||||
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
|
const socksOptions = {
|
||||||
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT,
|
||||||
|
username: config.SOCKS5PROXY.USERNAME || 'circuit0',
|
||||||
|
password: config.SOCKS5PROXY.PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AxiosResponse = await axios.get(path, axiosOptions);
|
||||||
|
if (data.statusText === 'error' || !data.data) {
|
||||||
|
throw new Error(`${data.status}`);
|
||||||
|
}
|
||||||
|
return data.data;
|
||||||
|
}
|
@ -401,7 +401,7 @@ class BlocksRepository {
|
|||||||
/**
|
/**
|
||||||
* Get average block health for all blocks for a single pool
|
* Get average block health for all blocks for a single pool
|
||||||
*/
|
*/
|
||||||
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
|
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number | null> {
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
const query = `
|
const query = `
|
||||||
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
|
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
|
||||||
@ -413,8 +413,8 @@ class BlocksRepository {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await DB.query(query, params);
|
const [rows] = await DB.query(query, params);
|
||||||
if (!rows[0] || !rows[0].avg_match_rate) {
|
if (!rows[0] || rows[0].avg_match_rate == null) {
|
||||||
return 0;
|
return null;
|
||||||
}
|
}
|
||||||
return Math.round(rows[0].avg_match_rate * 100) / 100;
|
return Math.round(rows[0].avg_match_rate * 100) / 100;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -3,7 +3,6 @@ import logger from '../../logger';
|
|||||||
import channelsApi from '../../api/explorer/channels.api';
|
import channelsApi from '../../api/explorer/channels.api';
|
||||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
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 { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||||
import { $lookupNodeLocation } from './sync-tasks/node-locations';
|
import { $lookupNodeLocation } from './sync-tasks/node-locations';
|
||||||
import lightningApi from '../../api/lightning/lightning-api-factory';
|
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||||
@ -269,7 +268,11 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async $scanForClosedChannels(): Promise<void> {
|
private async $scanForClosedChannels(): Promise<void> {
|
||||||
if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) {
|
let currentBlockHeight = blocks.getCurrentBlockHeight();
|
||||||
|
if (config.MEMPOOL.ENABLED === false) { // https://github.com/mempool/mempool/issues/3582
|
||||||
|
currentBlockHeight = await bitcoinApi.$getBlockHeightTip();
|
||||||
|
}
|
||||||
|
if (this.closedChannelsScanBlock === currentBlockHeight) {
|
||||||
logger.debug(`We've already scan closed channels for this block, skipping.`);
|
logger.debug(`We've already scan closed channels for this block, skipping.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -305,7 +308,7 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
|
this.closedChannelsScanBlock = currentBlockHeight;
|
||||||
logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln);
|
logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
|
@ -153,6 +153,7 @@ class PriceUpdater {
|
|||||||
try {
|
try {
|
||||||
const p = 60 * 60 * 1000; // milliseconds in an hour
|
const p = 60 * 60 * 1000; // milliseconds in an hour
|
||||||
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
|
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
|
||||||
|
this.latestPrices.time = nowRounded.getTime() / 1000;
|
||||||
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
|
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = previousRun + 5 * 60;
|
this.lastRun = previousRun + 5 * 60;
|
||||||
|
@ -27,3 +27,69 @@ export function formatBytes(bytes: number, toUnit: string, skipUnit = false): st
|
|||||||
|
|
||||||
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
|
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/64235212
|
||||||
|
export function hex2bin(hex: string): string {
|
||||||
|
if (!hex) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
hex = hex.replace('0x', '').toLowerCase();
|
||||||
|
let out = '';
|
||||||
|
|
||||||
|
for (const c of hex) {
|
||||||
|
switch (c) {
|
||||||
|
case '0': out += '0000'; break;
|
||||||
|
case '1': out += '0001'; break;
|
||||||
|
case '2': out += '0010'; break;
|
||||||
|
case '3': out += '0011'; break;
|
||||||
|
case '4': out += '0100'; break;
|
||||||
|
case '5': out += '0101'; break;
|
||||||
|
case '6': out += '0110'; break;
|
||||||
|
case '7': out += '0111'; break;
|
||||||
|
case '8': out += '1000'; break;
|
||||||
|
case '9': out += '1001'; break;
|
||||||
|
case 'a': out += '1010'; break;
|
||||||
|
case 'b': out += '1011'; break;
|
||||||
|
case 'c': out += '1100'; break;
|
||||||
|
case 'd': out += '1101'; break;
|
||||||
|
case 'e': out += '1110'; break;
|
||||||
|
case 'f': out += '1111'; break;
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bin2hex(bin: string): string {
|
||||||
|
if (!bin) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < bin.length; i += 4) {
|
||||||
|
const c = bin.substring(i, i + 4);
|
||||||
|
switch (c) {
|
||||||
|
case '0000': out += '0'; break;
|
||||||
|
case '0001': out += '1'; break;
|
||||||
|
case '0010': out += '2'; break;
|
||||||
|
case '0011': out += '3'; break;
|
||||||
|
case '0100': out += '4'; break;
|
||||||
|
case '0101': out += '5'; break;
|
||||||
|
case '0110': out += '6'; break;
|
||||||
|
case '0111': out += '7'; break;
|
||||||
|
case '1000': out += '8'; break;
|
||||||
|
case '1001': out += '9'; break;
|
||||||
|
case '1010': out += 'a'; break;
|
||||||
|
case '1011': out += 'b'; break;
|
||||||
|
case '1100': out += 'c'; break;
|
||||||
|
case '1101': out += 'd'; break;
|
||||||
|
case '1110': out += 'e'; break;
|
||||||
|
case '1111': out += 'f'; break;
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
3
contributors/bennyhodl.txt
Normal file
3
contributors/bennyhodl.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 28, 2023.
|
||||||
|
|
||||||
|
Signed: bennyhodl
|
3
contributors/pfoytik.txt
Normal file
3
contributors/pfoytik.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 15, 2023.
|
||||||
|
|
||||||
|
Signed pfoytik
|
3
contributors/secondl1ght.txt
Normal file
3
contributors/secondl1ght.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 14, 2023.
|
||||||
|
|
||||||
|
Signed: secondl1ght
|
@ -144,8 +144,8 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_ADVANCED_GBT_AUDIT: ""
|
MEMPOOL_ADVANCED_GBT_AUDIT: ""
|
||||||
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
||||||
MEMPOOL_CPFP_INDEXING: ""
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
MAX_BLOCKS_BULK_QUERY: ""
|
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||||
DISK_CACHE_BLOCK_INTERVAL: ""
|
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
||||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
|
||||||
|
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
|
||||||
|
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
|
||||||
},
|
},
|
||||||
@ -125,5 +127,11 @@
|
|||||||
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
|
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
|
||||||
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
|
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
|
||||||
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
|
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": __REPLICATION_ENABLED__,
|
||||||
|
"AUDIT": __REPLICATION_AUDIT__,
|
||||||
|
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||||
|
"SERVERS": __REPLICATION_SERVERS__
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,6 +32,9 @@ __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
|||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
||||||
|
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
|
||||||
|
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
|
||||||
|
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
@ -127,6 +130,12 @@ __MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City
|
|||||||
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
|
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
|
||||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
||||||
|
|
||||||
|
# REPLICATION
|
||||||
|
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=true}
|
||||||
|
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
||||||
|
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||||
|
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||||
|
|
||||||
|
|
||||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||||
|
|
||||||
@ -161,6 +170,8 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" me
|
|||||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
|
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
|
||||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
||||||
@ -245,5 +256,10 @@ sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-conf
|
|||||||
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
|
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
|
||||||
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
|
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
|
||||||
|
|
||||||
|
# REPLICATION
|
||||||
|
sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||||
|
|
||||||
node /backend/package/index.js
|
node /backend/package/index.js
|
||||||
|
@ -17,7 +17,7 @@ Get the latest Mempool code:
|
|||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/mempool/mempool
|
git clone https://github.com/mempool/mempool
|
||||||
cd mempool
|
cd mempool/frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Specify Website
|
### 2. Specify Website
|
||||||
|
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "^14.2.10",
|
"@angular-devkit/build-angular": "^14.2.10",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "2.6.0-dev",
|
"version": "3.0.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
|
@ -22,6 +22,7 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
|
|||||||
import { AssetsComponent } from './components/assets/assets.component';
|
import { AssetsComponent } from './components/assets/assets.component';
|
||||||
import { AssetComponent } from './components/asset/asset.component';
|
import { AssetComponent } from './components/asset/asset.component';
|
||||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||||
|
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -278,6 +279,10 @@ let routes: Routes = [
|
|||||||
path: 'rbf',
|
path: 'rbf',
|
||||||
component: RbfList,
|
component: RbfList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tools/calculator',
|
||||||
|
component: CalculatorComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
component: TermsOfServiceComponent
|
component: TermsOfServiceComponent
|
||||||
|
@ -112,7 +112,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block)));
|
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||||
|
|
||||||
this.stateService.bsqPrice$
|
this.stateService.bsqPrice$
|
||||||
.subscribe((bsqPrice) => {
|
.subscribe((bsqPrice) => {
|
||||||
|
@ -27,7 +27,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<span i18n="shared.address">Address</span>
|
<span i18n="shared.address">Address</span>
|
||||||
</app-preview-title>
|
</app-preview-title>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md table-col">
|
||||||
<div class="row d-flex justify-content-between">
|
<div class="row d-flex justify-content-between">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
|
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
|
||||||
|
@ -20,6 +20,11 @@
|
|||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-col {
|
||||||
|
max-width: calc(100% - 470px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
margin-top: 48px;
|
margin-top: 48px;
|
||||||
|
@ -207,7 +207,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.retryLoadMore = false;
|
this.retryLoadMore = false;
|
||||||
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.lastTransactionTxId)
|
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
|
||||||
.subscribe((transactions: Transaction[]) => {
|
.subscribe((transactions: Transaction[]) => {
|
||||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||||
this.loadedConfirmedTxCount += transactions.length;
|
this.loadedConfirmedTxCount += transactions.length;
|
||||||
@ -217,6 +217,10 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
(error) => {
|
(error) => {
|
||||||
this.isLoadingTransactions = false;
|
this.isLoadingTransactions = false;
|
||||||
this.retryLoadMore = true;
|
this.retryLoadMore = true;
|
||||||
|
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
|
||||||
|
if (error.status === 422) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
|
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
|
||||||
<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) && !unavailable">
|
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||||
<div *ngIf="isLoading" 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 *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-block-overview-tooltip
|
<app-block-overview-tooltip
|
||||||
[tx]="selectedTx || hoverTx"
|
[tx]="selectedTx || hoverTx"
|
||||||
[cursorPosition]="tooltipPosition"
|
[cursorPosition]="tooltipPosition"
|
||||||
@ -13,3 +14,4 @@
|
|||||||
[blockConversion]="blockConversion"
|
[blockConversion]="blockConversion"
|
||||||
></app-block-overview-tooltip>
|
></app-block-overview-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -6,8 +6,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-column: 1/-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-align {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, 75px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.block-overview-canvas {
|
.block-overview-canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -6,6 +6,8 @@ import TxSprite from './tx-sprite';
|
|||||||
import TxView from './tx-view';
|
import TxView from './tx-view';
|
||||||
import { Position } from './sprite-types';
|
import { Position } from './sprite-types';
|
||||||
import { Price } from '../../services/price.service';
|
import { Price } from '../../services/price.service';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-overview-graph',
|
selector: 'app-block-overview-graph',
|
||||||
@ -23,7 +25,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() unavailable: boolean = false;
|
@Input() unavailable: boolean = false;
|
||||||
@Input() auditHighlighting: boolean = false;
|
@Input() auditHighlighting: boolean = false;
|
||||||
@Input() blockConversion: Price;
|
@Input() blockConversion: Price;
|
||||||
@Input() pixelAlign: boolean = false;
|
|
||||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||||
@Output() txHoverEvent = new EventEmitter<string>();
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
@ -44,16 +45,25 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
scene: BlockScene;
|
scene: BlockScene;
|
||||||
hoverTx: TxView | void;
|
hoverTx: TxView | void;
|
||||||
selectedTx: TxView | void;
|
selectedTx: TxView | void;
|
||||||
|
highlightTx: TxView | void;
|
||||||
mirrorTx: TxView | void;
|
mirrorTx: TxView | void;
|
||||||
tooltipPosition: Position;
|
tooltipPosition: Position;
|
||||||
|
|
||||||
readyNextFrame = false;
|
readyNextFrame = false;
|
||||||
|
|
||||||
|
searchText: string;
|
||||||
|
searchSubscription: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly ngZone: NgZone,
|
readonly ngZone: NgZone,
|
||||||
readonly elRef: ElementRef,
|
readonly elRef: ElementRef,
|
||||||
|
private stateService: StateService,
|
||||||
) {
|
) {
|
||||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||||
|
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
|
||||||
|
this.searchText = text;
|
||||||
|
this.updateSearchHighlight();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
@ -109,6 +119,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.scene.setup(transactions);
|
this.scene.setup(transactions);
|
||||||
this.readyNextFrame = true;
|
this.readyNextFrame = true;
|
||||||
this.start();
|
this.start();
|
||||||
|
this.updateSearchHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +127,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.enter(transactions, direction);
|
this.scene.enter(transactions, direction);
|
||||||
this.start();
|
this.start();
|
||||||
|
this.updateSearchHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +135,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.exit(direction);
|
this.scene.exit(direction);
|
||||||
this.start();
|
this.start();
|
||||||
|
this.updateSearchHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +143,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.replace(transactions || [], direction, sort);
|
this.scene.replace(transactions || [], direction, sort);
|
||||||
this.start();
|
this.start();
|
||||||
|
this.updateSearchHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +151,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.update(add, remove, change, direction, resetLayout);
|
this.scene.update(add, remove, change, direction, resetLayout);
|
||||||
this.start();
|
this.start();
|
||||||
|
this.updateSearchHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +218,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
} else {
|
} else {
|
||||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||||
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
|
highlighting: this.auditHighlighting });
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -406,6 +421,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSearchHighlight(): void {
|
||||||
|
if (this.highlightTx && this.highlightTx.txid !== this.searchText && this.scene) {
|
||||||
|
this.scene.setHighlight(this.highlightTx, false);
|
||||||
|
this.start();
|
||||||
|
} else if (this.scene?.txs && this.searchText && this.searchText.length === 64) {
|
||||||
|
this.highlightTx = this.scene.txs[this.searchText];
|
||||||
|
if (this.highlightTx) {
|
||||||
|
this.scene.setHighlight(this.highlightTx, true);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setHighlightingEnabled(enabled: boolean): void {
|
setHighlightingEnabled(enabled: boolean): void {
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.setHighlighting(enabled);
|
this.scene.setHighlighting(enabled);
|
||||||
|
@ -15,7 +15,6 @@ export default class BlockScene {
|
|||||||
gridWidth: number;
|
gridWidth: number;
|
||||||
gridHeight: number;
|
gridHeight: number;
|
||||||
gridSize: number;
|
gridSize: number;
|
||||||
pixelAlign: boolean;
|
|
||||||
vbytesPerUnit: number;
|
vbytesPerUnit: number;
|
||||||
unitPadding: number;
|
unitPadding: number;
|
||||||
unitWidth: number;
|
unitWidth: number;
|
||||||
@ -24,24 +23,19 @@ export default class BlockScene {
|
|||||||
animateUntil = 0;
|
animateUntil = 0;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||||
) {
|
) {
|
||||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
|
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gridSize = this.width / this.gridWidth;
|
this.gridSize = this.width / this.gridWidth;
|
||||||
if (this.pixelAlign) {
|
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
|
||||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5));
|
|
||||||
this.unitWidth = this.gridSize - (this.unitPadding);
|
|
||||||
} else {
|
|
||||||
this.unitPadding = width / 500;
|
|
||||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||||
}
|
|
||||||
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
if (this.initialised && this.scene) {
|
if (this.initialised && this.scene) {
|
||||||
@ -215,15 +209,18 @@ export default class BlockScene {
|
|||||||
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
setHighlight(tx: TxView, value: boolean): void {
|
||||||
|
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||||
): void {
|
): void {
|
||||||
this.orientation = orientation;
|
this.orientation = orientation;
|
||||||
this.flip = flip;
|
this.flip = flip;
|
||||||
this.vertexArray = vertexArray;
|
this.vertexArray = vertexArray;
|
||||||
this.highlightingEnabled = highlighting;
|
this.highlightingEnabled = highlighting;
|
||||||
this.pixelAlign = pixelAlign;
|
|
||||||
|
|
||||||
this.scene = {
|
this.scene = {
|
||||||
count: 0,
|
count: 0,
|
||||||
@ -349,12 +346,7 @@ export default class BlockScene {
|
|||||||
private gridToScreen(position: Square | void): Square {
|
private gridToScreen(position: Square | void): Square {
|
||||||
if (position) {
|
if (position) {
|
||||||
const slotSize = (position.s * this.gridSize);
|
const slotSize = (position.s * this.gridSize);
|
||||||
let squareSize;
|
const squareSize = slotSize - (this.unitPadding * 2);
|
||||||
if (this.pixelAlign) {
|
|
||||||
squareSize = slotSize - (this.unitPadding);
|
|
||||||
} else {
|
|
||||||
squareSize = slotSize - (this.unitPadding * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The grid is laid out notionally left-to-right, bottom-to-top,
|
// The grid is laid out notionally left-to-right, bottom-to-top,
|
||||||
// so we rotate and/or flip the y axis to match the target configuration.
|
// so we rotate and/or flip the y axis to match the target configuration.
|
||||||
@ -430,7 +422,7 @@ export default class BlockScene {
|
|||||||
|
|
||||||
// calculates and returns the size of the tx in multiples of the grid size
|
// calculates and returns the size of the tx in multiples of the grid size
|
||||||
private txSize(tx: TxView): number {
|
private txSize(tx: TxView): number {
|
||||||
const scale = Math.max(1, Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)));
|
const scale = Math.max(1, Math.round(Math.sqrt(1.1 * tx.vsize / this.vbytesPerUnit)));
|
||||||
return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
|
return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import BlockScene from './block-scene';
|
|||||||
|
|
||||||
const hoverTransitionTime = 300;
|
const hoverTransitionTime = 300;
|
||||||
const defaultHoverColor = hexToColor('1bd8f4');
|
const defaultHoverColor = hexToColor('1bd8f4');
|
||||||
|
const defaultHighlightColor = hexToColor('800080');
|
||||||
|
|
||||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||||
@ -37,15 +38,17 @@ export default class TxView implements TransactionStripped {
|
|||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
scene?: BlockScene;
|
scene?: BlockScene;
|
||||||
|
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
|
highlight: boolean;
|
||||||
sprite: TxSprite;
|
sprite: TxSprite;
|
||||||
hoverColor: Color | void;
|
hoverColor: Color | void;
|
||||||
|
highlightColor: Color | void;
|
||||||
|
|
||||||
screenPosition: Square;
|
screenPosition: Square;
|
||||||
gridPosition: Square | void;
|
gridPosition: Square | void;
|
||||||
@ -150,10 +153,42 @@ export default class TxView implements TransactionStripped {
|
|||||||
} else {
|
} else {
|
||||||
this.hover = false;
|
this.hover = false;
|
||||||
this.hoverColor = null;
|
this.hoverColor = null;
|
||||||
|
if (this.highlight) {
|
||||||
|
this.setHighlight(true, this.highlightColor);
|
||||||
|
} else {
|
||||||
if (this.sprite) {
|
if (this.sprite) {
|
||||||
this.sprite.resume(hoverTransitionTime);
|
this.sprite.resume(hoverTransitionTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
this.dirty = false;
|
||||||
|
return performance.now() + hoverTransitionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily override the tx color
|
||||||
|
// returns minimum transition end time
|
||||||
|
setHighlight(highlightOn: boolean, color: Color | void = defaultHighlightColor): number {
|
||||||
|
if (highlightOn) {
|
||||||
|
this.highlight = true;
|
||||||
|
this.highlightColor = color;
|
||||||
|
|
||||||
|
this.sprite.update({
|
||||||
|
...this.highlightColor,
|
||||||
|
duration: hoverTransitionTime,
|
||||||
|
adjust: false,
|
||||||
|
temp: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.highlight = false;
|
||||||
|
this.highlightColor = null;
|
||||||
|
if (this.hover) {
|
||||||
|
this.setHover(true, this.hoverColor);
|
||||||
|
} else {
|
||||||
|
if (this.sprite) {
|
||||||
|
this.sprite.resume(hoverTransitionTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
return performance.now() + hoverTransitionTime;
|
return performance.now() + hoverTransitionTime;
|
||||||
}
|
}
|
||||||
@ -175,6 +210,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
case 'fullrbf':
|
case 'fullrbf':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'fresh':
|
case 'fresh':
|
||||||
|
case 'freshcpfp':
|
||||||
return auditColors.missing;
|
return auditColors.missing;
|
||||||
case 'added':
|
case 'added':
|
||||||
return auditColors.added;
|
return auditColors.added;
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
|
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
|
||||||
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
||||||
|
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<span i18n="shared.block-title">Block</span>
|
<span i18n="shared.block-title">Block</span>
|
||||||
</app-preview-title>
|
</app-preview-title>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm table-col">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="block-titles">
|
<div class="block-titles">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
@ -71,7 +71,7 @@
|
|||||||
<app-block-overview-graph
|
<app-block-overview-graph
|
||||||
#blockGraph
|
#blockGraph
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
[resolution]="75"
|
[resolution]="80"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="'top'"
|
[orientation]="'top'"
|
||||||
[flip]="false"
|
[flip]="false"
|
||||||
|
@ -44,11 +44,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-col {
|
||||||
|
max-width: calc(100% - 470px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 470px;
|
width: 480px;
|
||||||
min-width: 470px;
|
min-width: 480px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm" [class.graph-col]="webGlEnabled && !showAudit">
|
||||||
<table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
|
<table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-container *ngTemplateOutlet="restOfTable"></ng-container>
|
<ng-container *ngTemplateOutlet="restOfTable"></ng-container>
|
||||||
@ -110,7 +110,7 @@
|
|||||||
<app-block-overview-graph
|
<app-block-overview-graph
|
||||||
#blockGraphActual
|
#blockGraphActual
|
||||||
[isLoading]="isLoadingOverview"
|
[isLoading]="isLoadingOverview"
|
||||||
[resolution]="75"
|
[resolution]="86"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="'top'"
|
[orientation]="'top'"
|
||||||
[flip]="false"
|
[flip]="false"
|
||||||
@ -227,7 +227,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||||
<div class="block-graph-wrapper">
|
<div class="block-graph-wrapper">
|
||||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||||
@ -239,7 +239,7 @@
|
|||||||
<div class="col-sm" *ngIf="!isMobile">
|
<div class="col-sm" *ngIf="!isMobile">
|
||||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||||
<div class="block-graph-wrapper">
|
<div class="block-graph-wrapper">
|
||||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||||
|
@ -239,6 +239,7 @@ h1 {
|
|||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link {
|
.nav-tabs .nav-link {
|
||||||
@ -293,3 +294,7 @@ h1 {
|
|||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-col {
|
||||||
|
flex-grow: 1.11;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ 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';
|
||||||
import { PriceService, Price } from '../../services/price.service';
|
import { PriceService, Price } from '../../services/price.service';
|
||||||
|
import { CacheService } from '../../services/cache.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
@ -72,6 +73,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
auditSubscription: Subscription;
|
auditSubscription: Subscription;
|
||||||
keyNavigationSubscription: Subscription;
|
keyNavigationSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
|
cacheBlocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
nextBlockSubscription: Subscription = undefined;
|
nextBlockSubscription: Subscription = undefined;
|
||||||
@ -99,6 +101,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
this.webGlEnabled = detectWebGL();
|
this.webGlEnabled = detectWebGL();
|
||||||
}
|
}
|
||||||
@ -128,13 +131,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
|
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||||
|
this.loadedCacheBlock(block);
|
||||||
|
});
|
||||||
|
|
||||||
this.blocksSubscription = this.stateService.blocks$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.subscribe(([block]) => {
|
.subscribe((blocks) => {
|
||||||
this.latestBlock = block;
|
this.latestBlock = blocks[0];
|
||||||
this.latestBlocks.unshift(block);
|
this.latestBlocks = blocks;
|
||||||
this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
|
||||||
this.setNextAndPreviousBlockLink();
|
this.setNextAndPreviousBlockLink();
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
if (block.id === this.blockHash) {
|
if (block.id === this.blockHash) {
|
||||||
this.block = block;
|
this.block = block;
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
@ -142,6 +149,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
} else if (block.height === this.block?.height) {
|
||||||
|
this.block.stale = true;
|
||||||
|
this.block.canonical = block.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -254,6 +265,13 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.transactionsError = null;
|
this.transactionsError = null;
|
||||||
this.isLoadingOverview = true;
|
this.isLoadingOverview = true;
|
||||||
this.overviewError = null;
|
this.overviewError = null;
|
||||||
|
|
||||||
|
const cachedBlock = this.cacheService.getCachedBlock(block.height);
|
||||||
|
if (!cachedBlock) {
|
||||||
|
this.cacheService.loadBlock(block.height);
|
||||||
|
} else {
|
||||||
|
this.loadedCacheBlock(cachedBlock);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
@ -352,7 +370,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
tx.status = 'found';
|
tx.status = 'found';
|
||||||
} else {
|
} else {
|
||||||
if (isFresh[tx.txid]) {
|
if (isFresh[tx.txid]) {
|
||||||
|
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
|
||||||
|
tx.status = 'freshcpfp';
|
||||||
|
} else {
|
||||||
tx.status = 'fresh';
|
tx.status = 'fresh';
|
||||||
|
}
|
||||||
} else if (isSigop[tx.txid]) {
|
} else if (isSigop[tx.txid]) {
|
||||||
tx.status = 'sigop';
|
tx.status = 'sigop';
|
||||||
} else if (isFullRbf[tx.txid]) {
|
} else if (isFullRbf[tx.txid]) {
|
||||||
@ -459,6 +481,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.auditSubscription?.unsubscribe();
|
this.auditSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription?.unsubscribe();
|
this.keyNavigationSubscription?.unsubscribe();
|
||||||
this.blocksSubscription?.unsubscribe();
|
this.blocksSubscription?.unsubscribe();
|
||||||
|
this.cacheBlocksSubscription?.unsubscribe();
|
||||||
this.networkChangedSubscription?.unsubscribe();
|
this.networkChangedSubscription?.unsubscribe();
|
||||||
this.queryParamsSubscription?.unsubscribe();
|
this.queryParamsSubscription?.unsubscribe();
|
||||||
this.timeLtrSubscription?.unsubscribe();
|
this.timeLtrSubscription?.unsubscribe();
|
||||||
@ -679,4 +702,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadedCacheBlock(block: BlockExtended): void {
|
||||||
|
if (this.block && block.height === this.block.height && block.id !== this.block.id) {
|
||||||
|
this.block.stale = true;
|
||||||
|
this.block.canonical = block.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -36,11 +36,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||||
markHeight: number;
|
markHeight: number;
|
||||||
chainTip: number;
|
chainTip: number;
|
||||||
|
pendingMarkBlock: { animate: boolean, newBlockFromLeft: boolean };
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
blockPageSubscription: Subscription;
|
blockPageSubscription: Subscription;
|
||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
tabHiddenSubscription: Subscription;
|
tabHiddenSubscription: Subscription;
|
||||||
markBlockSubscription: Subscription;
|
markBlockSubscription: Subscription;
|
||||||
|
txConfirmedSubscription: Subscription;
|
||||||
loadingBlocks$: Observable<boolean>;
|
loadingBlocks$: Observable<boolean>;
|
||||||
blockStyles = [];
|
blockStyles = [];
|
||||||
emptyBlockStyles = [];
|
emptyBlockStyles = [];
|
||||||
@ -82,7 +84,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.chainTip = this.stateService.latestBlockHeight;
|
|
||||||
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
@ -104,31 +105,22 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||||
if (!this.static) {
|
if (!this.static) {
|
||||||
this.blocksSubscription = this.stateService.blocks$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.subscribe(([block, txConfirmed]) => {
|
.subscribe((blocks) => {
|
||||||
if (this.blocks.some((b) => b.height === block.height)) {
|
if (!blocks?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const latestHeight = blocks[0].height;
|
||||||
|
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
||||||
|
|
||||||
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
|
for (const block of blocks) {
|
||||||
this.blocks = [];
|
|
||||||
this.blocksFilled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
block.extras.minFee = this.getMinBlockFee(block);
|
block.extras.minFee = this.getMinBlockFee(block);
|
||||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||||
|
|
||||||
this.blocks.unshift(block);
|
|
||||||
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
|
||||||
|
|
||||||
if (txConfirmed && block.height > this.chainTip) {
|
|
||||||
this.markHeight = block.height;
|
|
||||||
this.moveArrowToPosition(true, true);
|
|
||||||
} else {
|
|
||||||
this.moveArrowToPosition(true, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.blocks = blocks;
|
||||||
|
|
||||||
this.blockStyles = [];
|
this.blockStyles = [];
|
||||||
if (this.blocksFilled && block.height > this.chainTip) {
|
if (animate) {
|
||||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.blockStyles = [];
|
this.blockStyles = [];
|
||||||
@ -139,13 +131,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.blocks.length === this.dynamicBlocksAmount) {
|
this.chainTip = latestHeight;
|
||||||
this.blocksFilled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chainTip = Math.max(this.chainTip, block.height);
|
if (this.pendingMarkBlock) {
|
||||||
|
this.moveArrowToPosition(this.pendingMarkBlock.animate, this.pendingMarkBlock.newBlockFromLeft);
|
||||||
|
this.pendingMarkBlock = null;
|
||||||
|
}
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txid, block]) => {
|
||||||
|
if (txid) {
|
||||||
|
this.markHeight = block.height;
|
||||||
|
this.moveArrowToPosition(true, true);
|
||||||
|
} else {
|
||||||
|
this.moveArrowToPosition(true, false);
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||||
if (block.height <= this.height && block.height > this.height - this.count) {
|
if (block.height <= this.height && block.height > this.height - this.count) {
|
||||||
@ -190,6 +192,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (this.blockPageSubscription) {
|
if (this.blockPageSubscription) {
|
||||||
this.blockPageSubscription.unsubscribe();
|
this.blockPageSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
if (this.txConfirmedSubscription) {
|
||||||
|
this.txConfirmedSubscription.unsubscribe();
|
||||||
|
}
|
||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.tabHiddenSubscription.unsubscribe();
|
this.tabHiddenSubscription.unsubscribe();
|
||||||
this.markBlockSubscription.unsubscribe();
|
this.markBlockSubscription.unsubscribe();
|
||||||
@ -202,6 +207,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.arrowVisible = false;
|
this.arrowVisible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.chainTip == null) {
|
||||||
|
this.pendingMarkBlock = { animate, newBlockFromLeft };
|
||||||
|
}
|
||||||
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
||||||
if (blockindex > -1) {
|
if (blockindex > -1) {
|
||||||
if (!animate) {
|
if (!animate) {
|
||||||
|
@ -82,12 +82,12 @@ export class BlocksList implements OnInit {
|
|||||||
),
|
),
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((block) => {
|
switchMap((blocks) => {
|
||||||
if (block[0].height <= this.lastBlockHeight) {
|
if (blocks[0].height <= this.lastBlockHeight) {
|
||||||
return [null]; // Return an empty stream so the last pipe is not executed
|
return [null]; // Return an empty stream so the last pipe is not executed
|
||||||
}
|
}
|
||||||
this.lastBlockHeight = block[0].height;
|
this.lastBlockHeight = blocks[0].height;
|
||||||
return [block];
|
return blocks;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
<div class="container-xl">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2>Calculator</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="price$ | async; else loading">
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div class="input-group input-group-lg mb-1">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||||
|
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group input-group-lg mb-1">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">BTC</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||||
|
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group input-group-lg mb-1">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">sats</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||||
|
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="bitcoin-satoshis-text">
|
||||||
|
₿
|
||||||
|
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
|
||||||
|
<span class="sats"> sats</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="fiat-text">
|
||||||
|
<app-fiat [value]="form.get('satoshis').value" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center mt-3">
|
||||||
|
<div class="symbol">
|
||||||
|
Fiat price last updated <app-time kind="since" [time]="lastFiatPrice$ | async" [fastRender]="true"></app-time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #loading>
|
||||||
|
<div class="text-center">
|
||||||
|
Waiting for price feed...
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</div>
|
@ -0,0 +1,30 @@
|
|||||||
|
.input-group-text {
|
||||||
|
width: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bitcoin-satoshis-text {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fiat-text {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.bitcoin-satoshis-text {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sats {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: auto;
|
||||||
|
}
|
137
frontend/src/app/components/calculator/calculator.component.ts
Normal file
137
frontend/src/app/components/calculator/calculator.component.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-calculator',
|
||||||
|
templateUrl: './calculator.component.html',
|
||||||
|
styleUrls: ['./calculator.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class CalculatorComponent implements OnInit {
|
||||||
|
satoshis = 10000;
|
||||||
|
form: FormGroup;
|
||||||
|
|
||||||
|
currency$ = this.stateService.fiatCurrency$;
|
||||||
|
price$: Observable<number>;
|
||||||
|
lastFiatPrice$: Observable<number>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
fiat: [0],
|
||||||
|
bitcoin: [0],
|
||||||
|
satoshis: [0],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastFiatPrice$ = this.stateService.conversions$.asObservable()
|
||||||
|
.pipe(
|
||||||
|
map((conversions) => conversions.time)
|
||||||
|
);
|
||||||
|
|
||||||
|
let currency;
|
||||||
|
this.price$ = this.currency$.pipe(
|
||||||
|
switchMap((result) => {
|
||||||
|
currency = result;
|
||||||
|
return this.stateService.conversions$.asObservable();
|
||||||
|
}),
|
||||||
|
map((conversions) => {
|
||||||
|
return conversions[currency];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
this.price$,
|
||||||
|
this.form.get('fiat').valueChanges
|
||||||
|
]).subscribe(([price, value]) => {
|
||||||
|
const rate = (value / price).toFixed(8);
|
||||||
|
const satsRate = Math.round(value / price * 100_000_000);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.get('bitcoin').setValue(rate, { emitEvent: false });
|
||||||
|
this.form.get('satoshis').setValue(satsRate, { emitEvent: false } );
|
||||||
|
});
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
this.price$,
|
||||||
|
this.form.get('bitcoin').valueChanges
|
||||||
|
]).subscribe(([price, value]) => {
|
||||||
|
const rate = parseFloat((value * price).toFixed(8));
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||||
|
this.form.get('satoshis').setValue(Math.round(value * 100_000_000), { emitEvent: false } );
|
||||||
|
});
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
this.price$,
|
||||||
|
this.form.get('satoshis').valueChanges
|
||||||
|
]).subscribe(([price, value]) => {
|
||||||
|
const rate = parseFloat((value / 100_000_000 * price).toFixed(8));
|
||||||
|
const bitcoinRate = (value / 100_000_000).toFixed(8);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||||
|
this.form.get('bitcoin').setValue(bitcoinRate, { emitEvent: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
transformInput(name: string): void {
|
||||||
|
const formControl = this.form.get(name);
|
||||||
|
if (!formControl.value) {
|
||||||
|
return formControl.setValue('', {emitEvent: false});
|
||||||
|
}
|
||||||
|
let value = formControl.value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||||
|
if (value === '.') {
|
||||||
|
value = '0';
|
||||||
|
}
|
||||||
|
let sanitizedValue = this.removeExtraDots(value);
|
||||||
|
if (name === 'bitcoin' && this.countDecimals(sanitizedValue) > 8) {
|
||||||
|
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, 8);
|
||||||
|
}
|
||||||
|
if (sanitizedValue === '') {
|
||||||
|
sanitizedValue = '0';
|
||||||
|
}
|
||||||
|
if (name === 'satoshis') {
|
||||||
|
sanitizedValue = parseFloat(sanitizedValue).toFixed(0);
|
||||||
|
}
|
||||||
|
formControl.setValue(sanitizedValue, {emitEvent: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExtraDots(str: string): string {
|
||||||
|
const [beforeDot, afterDot] = str.split('.', 2);
|
||||||
|
if (afterDot === undefined) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
const afterDotReplaced = afterDot.replace(/\./g, '');
|
||||||
|
return `${beforeDot}.${afterDotReplaced}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
countDecimals(numberString: string): number {
|
||||||
|
const decimalPos = numberString.indexOf('.');
|
||||||
|
if (decimalPos === -1) return 0;
|
||||||
|
return numberString.length - decimalPos - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toFixedWithoutRounding(numStr: string, fixed: number): string {
|
||||||
|
const re = new RegExp(`^-?\\d+(?:.\\d{0,${(fixed || -1)}})?`);
|
||||||
|
const result = numStr.match(re);
|
||||||
|
return result ? result[0] : numStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll(event): void {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||||
{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
|
‎{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
|
||||||
</span>
|
</span>
|
||||||
|
@ -39,13 +39,10 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
this.blocksSubscription = this.stateService.blocks$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.subscribe(([block]) => {
|
.subscribe((blocks) => {
|
||||||
if (block) {
|
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
|
||||||
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
|
|
||||||
// using block-reported times, so ensure they are sorted chronologically
|
|
||||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||||
this.updateSegments();
|
this.updateSegments();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #mempoolMode>
|
<ng-template #mempoolMode>
|
||||||
<div class="block-sizer" [style]="blockSizerStyle">
|
<div class="block-sizer" [style]="blockSizerStyle">
|
||||||
<app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview>
|
<app-mempool-block-overview [index]="blockIndex"></app-mempool-block-overview>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="fader"></div>
|
<div class="fader"></div>
|
||||||
|
@ -60,15 +60,12 @@ export class ClockComponent implements OnInit {
|
|||||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||||
|
|
||||||
this.blocksSubscription = this.stateService.blocks$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.subscribe(([block]) => {
|
.subscribe((blocks) => {
|
||||||
if (block) {
|
this.blocks = blocks.slice(0, 16);
|
||||||
this.blocks.unshift(block);
|
|
||||||
this.blocks = this.blocks.slice(0, 16);
|
|
||||||
if (this.blocks[this.blockIndex]) {
|
if (this.blocks[this.blockIndex]) {
|
||||||
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.recommendedFees$ = this.stateService.recommendedFees$;
|
this.recommendedFees$ = this.stateService.recommendedFees$;
|
||||||
|
@ -38,11 +38,12 @@ export class DifficultyMiningComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.difficultyEpoch$ = combineLatest([
|
this.difficultyEpoch$ = combineLatest([
|
||||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
this.stateService.blocks$,
|
||||||
this.stateService.difficultyAdjustment$,
|
this.stateService.difficultyAdjustment$,
|
||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([block, da]) => {
|
map(([blocks, da]) => {
|
||||||
|
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||||
let colorAdjustments = '#ffffff66';
|
let colorAdjustments = '#ffffff66';
|
||||||
if (da.difficultyChange > 0) {
|
if (da.difficultyChange > 0) {
|
||||||
colorAdjustments = '#3bcc49';
|
colorAdjustments = '#3bcc49';
|
||||||
@ -63,7 +64,7 @@ export class DifficultyMiningComponent implements OnInit {
|
|||||||
colorPreviousAdjustments = '#ffffff66';
|
colorPreviousAdjustments = '#ffffff66';
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -67,11 +67,12 @@ export class DifficultyComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.difficultyEpoch$ = combineLatest([
|
this.difficultyEpoch$ = combineLatest([
|
||||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
this.stateService.blocks$,
|
||||||
this.stateService.difficultyAdjustment$,
|
this.stateService.difficultyAdjustment$,
|
||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([block, da]) => {
|
map(([blocks, da]) => {
|
||||||
|
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||||
let colorAdjustments = '#ffffff66';
|
let colorAdjustments = '#ffffff66';
|
||||||
if (da.difficultyChange > 0) {
|
if (da.difficultyChange > 0) {
|
||||||
colorAdjustments = '#3bcc49';
|
colorAdjustments = '#3bcc49';
|
||||||
@ -92,7 +93,7 @@ export class DifficultyComponent implements OnInit {
|
|||||||
colorPreviousAdjustments = '#ffffff66';
|
colorPreviousAdjustments = '#ffffff66';
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||||
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
|
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
|
||||||
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
|
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
|
||||||
|
@ -109,6 +109,14 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
tap((response: any) => {
|
tap((response: any) => {
|
||||||
const data = response.body;
|
const data = response.body;
|
||||||
|
|
||||||
|
// always include the latest difficulty
|
||||||
|
if (data.difficulty.length && data.difficulty[data.difficulty.length - 1].difficulty !== data.currentDifficulty) {
|
||||||
|
data.difficulty.push({
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
difficulty: data.currentDifficulty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// We generate duplicated data point so the tooltip works nicely
|
// We generate duplicated data point so the tooltip works nicely
|
||||||
const diffFixed = [];
|
const diffFixed = [];
|
||||||
let diffIndex = 1;
|
let diffIndex = 1;
|
||||||
@ -122,6 +130,7 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
++hashIndex;
|
++hashIndex;
|
||||||
}
|
}
|
||||||
|
diffIndex++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +146,14 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
++diffIndex;
|
++diffIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (diffIndex <= data.difficulty.length) {
|
||||||
|
diffFixed.push({
|
||||||
|
timestamp: data.difficulty[diffIndex - 1].time,
|
||||||
|
difficulty: data.difficulty[diffIndex - 1].difficulty
|
||||||
|
});
|
||||||
|
diffIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
let maResolution = 15;
|
let maResolution = 15;
|
||||||
const hashrateMa = [];
|
const hashrateMa = [];
|
||||||
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
<app-block-overview-graph
|
<app-block-overview-graph
|
||||||
#blockGraph
|
#blockGraph
|
||||||
[isLoading]="isLoading$ | async"
|
[isLoading]="isLoading$ | async"
|
||||||
[resolution]="75"
|
[resolution]="86"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="timeLtr ? 'right' : 'left'"
|
[orientation]="timeLtr ? 'right' : 'left'"
|
||||||
[flip]="true"
|
[flip]="true"
|
||||||
[pixelAlign]="pixelAlign"
|
|
||||||
(txClickEvent)="onTxClick($event)"
|
(txClickEvent)="onTxClick($event)"
|
||||||
></app-block-overview-graph>
|
></app-block-overview-graph>
|
||||||
|
@ -16,7 +16,6 @@ import { Router } from '@angular/router';
|
|||||||
})
|
})
|
||||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||||
@Input() index: number;
|
@Input() index: number;
|
||||||
@Input() pixelAlign: boolean = false;
|
|
||||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
@ -124,7 +124,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => combineLatest([
|
switchMap(() => combineLatest([
|
||||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
||||||
this.stateService.mempoolBlocks$
|
this.stateService.mempoolBlocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((mempoolBlocks) => {
|
map((mempoolBlocks) => {
|
||||||
@ -186,8 +186,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.blockSubscription = this.stateService.blocks$
|
this.blockSubscription = this.stateService.blocks$.pipe(map((blocks) => blocks[0]))
|
||||||
.subscribe(([block]) => {
|
.subscribe((block) => {
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.chainTip === -1) {
|
if (this.chainTip === -1) {
|
||||||
this.animateEntry = block.height === this.stateService.latestBlockHeight;
|
this.animateEntry = block.height === this.stateService.latestBlockHeight;
|
||||||
} else {
|
} else {
|
||||||
@ -221,8 +224,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||||
} else {
|
} else {
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
|
.pipe(map((blocks) => blocks[0]))
|
||||||
.subscribe(([block]) => {
|
.subscribe((block) => {
|
||||||
if (this.stateService.latestBlockHeight === block.height) {
|
if (this.stateService.latestBlockHeight === block.height) {
|
||||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||||
}
|
}
|
||||||
@ -297,7 +300,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
while (blocks.length > blocksAmount) {
|
while (blocks.length > blocksAmount) {
|
||||||
const block = blocks.pop();
|
const block = blocks.pop();
|
||||||
if (!this.count) {
|
if (!this.count) {
|
||||||
const lastBlock = blocks[blocks.length - 1];
|
const lastBlock = blocks[0];
|
||||||
lastBlock.blockSize += block.blockSize;
|
lastBlock.blockSize += block.blockSize;
|
||||||
lastBlock.blockVSize += block.blockVSize;
|
lastBlock.blockVSize += block.blockVSize;
|
||||||
lastBlock.nTx += block.nTx;
|
lastBlock.nTx += block.nTx;
|
||||||
@ -308,7 +311,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (blocks.length) {
|
if (blocks.length) {
|
||||||
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
|
blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
|
||||||
}
|
}
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ export class PoolComponent implements OnInit {
|
|||||||
return this.apiService.getPoolStats$(slug);
|
return this.apiService.getPoolStats$(slug);
|
||||||
}),
|
}),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
this.loadMoreSubject.next(this.blocks[0]?.height);
|
||||||
}),
|
}),
|
||||||
map((poolStats) => {
|
map((poolStats) => {
|
||||||
this.seoService.setTitle(poolStats.pool.name);
|
this.seoService.setTitle(poolStats.pool.name);
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
|
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
|
||||||
<td>
|
<td>
|
||||||
|
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
|
||||||
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
|
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
|
||||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
||||||
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||||
import { RbfInfo } from '../../interfaces/node-api.interface';
|
import { RbfTree } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rbf-timeline-tooltip',
|
selector: 'app-rbf-timeline-tooltip',
|
||||||
@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface';
|
|||||||
styleUrls: ['./rbf-timeline-tooltip.component.scss'],
|
styleUrls: ['./rbf-timeline-tooltip.component.scss'],
|
||||||
})
|
})
|
||||||
export class RbfTimelineTooltipComponent implements OnChanges {
|
export class RbfTimelineTooltipComponent implements OnChanges {
|
||||||
@Input() rbfInfo: RbfInfo | void;
|
@Input() rbfInfo: RbfTree | null;
|
||||||
@Input() cursorPosition: { x: number, y: number };
|
@Input() cursorPosition: { x: number, y: number };
|
||||||
|
|
||||||
tooltipPosition = null;
|
tooltipPosition = null;
|
||||||
|
@ -15,14 +15,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="nodes">
|
<div class="nodes">
|
||||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||||
<ng-container *ngIf="cell.replacement; else nonNode">
|
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
|
||||||
<div class="node"
|
<div class="node"
|
||||||
[id]="'node-'+cell.replacement.tx.txid"
|
[id]="'node-'+cell.replacement.tx.txid"
|
||||||
[class.selected]="txid === cell.replacement.tx.txid"
|
[class.selected]="txid === cell.replacement.tx.txid"
|
||||||
[class.mined]="cell.replacement.tx.mined"
|
[class.mined]="cell.replacement.tx.mined"
|
||||||
[class.first-node]="cell.first"
|
[class.first-node]="cell.first"
|
||||||
>
|
>
|
||||||
<div class="track"></div>
|
<div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
|
||||||
|
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
|
||||||
<a class="shape-border"
|
<a class="shape-border"
|
||||||
[class.rbf]="cell.replacement.tx.rbf"
|
[class.rbf]="cell.replacement.tx.rbf"
|
||||||
[routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
|
[routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
|
||||||
@ -36,14 +37,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #nonNode>
|
<ng-template #nonNode>
|
||||||
<ng-container [ngSwitch]="cell.connector">
|
<ng-container [ngSwitch]="cell.connector">
|
||||||
<div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div>
|
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div>
|
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||||
<div class="node-spacer" *ngSwitchDefault></div>
|
<div class="node-spacer" *ngSwitchDefault></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-container *ngIf="i < timeline.length - 1">
|
<ng-container *ngIf="i < timeline.length - 1">
|
||||||
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
|
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
|
||||||
<div class="track"></div>
|
<div class="track" [class.fullrbf]="cell.fullRbf"></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -83,15 +83,26 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
background: #105fb0;
|
background: #105fb0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullrbf {
|
||||||
|
background: #1bd8f4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.first-node {
|
&.first-node {
|
||||||
.track {
|
.track.left {
|
||||||
left: 50%;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
.track {
|
.track.right {
|
||||||
right: 50%;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,11 +188,17 @@
|
|||||||
height: 108px;
|
height: 108px;
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
border-right: solid 10px #105fb0;
|
border-right: solid 10px #105fb0;
|
||||||
|
&.fullrbf {
|
||||||
|
border-right: solid 10px #1bd8f4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner {
|
.corner {
|
||||||
border-bottom: solid 10px #105fb0;
|
border-bottom: solid 10px #105fb0;
|
||||||
border-bottom-right-radius: 10px;
|
border-bottom-right-radius: 10px;
|
||||||
|
&.fullrbf {
|
||||||
|
border-bottom: solid 10px #1bd8f4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface';
|
import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
|
|
||||||
type Connector = 'pipe' | 'corner';
|
type Connector = 'pipe' | 'corner';
|
||||||
|
|
||||||
interface TimelineCell {
|
interface TimelineCell {
|
||||||
replacement?: RbfInfo,
|
replacement?: RbfTree,
|
||||||
connector?: Connector,
|
connector?: Connector,
|
||||||
first?: boolean,
|
first?: boolean,
|
||||||
|
fullRbf?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTimelineCell(val: RbfTree | TimelineCell): boolean {
|
||||||
|
return !val || !('tx' in val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
@Input() txid: string;
|
@Input() txid: string;
|
||||||
rows: TimelineCell[][] = [];
|
rows: TimelineCell[][] = [];
|
||||||
|
|
||||||
hoverInfo: RbfInfo | void = null;
|
hoverInfo: RbfTree | null = null;
|
||||||
tooltipPosition = null;
|
tooltipPosition = null;
|
||||||
|
|
||||||
dir: 'rtl' | 'ltr' = 'ltr';
|
dir: 'rtl' | 'ltr' = 'ltr';
|
||||||
@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
buildTimelines(tree: RbfTree): TimelineCell[][] {
|
buildTimelines(tree: RbfTree): TimelineCell[][] {
|
||||||
if (!tree) return [];
|
if (!tree) return [];
|
||||||
|
|
||||||
|
this.flagFullRbf(tree);
|
||||||
const split = this.splitTimelines(tree);
|
const split = this.splitTimelines(tree);
|
||||||
const timelines = this.prepareTimelines(split);
|
const timelines = this.prepareTimelines(split);
|
||||||
return this.connectTimelines(timelines);
|
return this.connectTimelines(timelines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sets the fullRbf flag on each transaction in the tree
|
||||||
|
flagFullRbf(tree: RbfTree): void {
|
||||||
|
let fullRbf = false;
|
||||||
|
for (const replaced of tree.replaces) {
|
||||||
|
if (!replaced.tx.rbf) {
|
||||||
|
fullRbf = true;
|
||||||
|
}
|
||||||
|
replaced.replacedBy = tree.tx;
|
||||||
|
this.flagFullRbf(replaced);
|
||||||
|
}
|
||||||
|
tree.tx.fullRbf = fullRbf;
|
||||||
|
}
|
||||||
|
|
||||||
// splits a tree into N leaf-to-root paths
|
// splits a tree into N leaf-to-root paths
|
||||||
splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] {
|
splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] {
|
||||||
const replacements = [...tail, tree];
|
const replacements = [...tail, tree];
|
||||||
if (tree.replaces.length) {
|
if (tree.replaces.length) {
|
||||||
return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
|
return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
|
||||||
@ -70,7 +89,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
// merges separate leaf-to-root paths into a coherent forking timeline
|
// merges separate leaf-to-root paths into a coherent forking timeline
|
||||||
// represented as a 2D array of Rbf events
|
// represented as a 2D array of Rbf events
|
||||||
prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] {
|
prepareTimelines(lines: RbfTree[][]): (RbfTree | TimelineCell)[][] {
|
||||||
lines.sort((a, b) => b.length - a.length);
|
lines.sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
const rows = lines.map(() => []);
|
const rows = lines.map(() => []);
|
||||||
@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
let emptyCount = 0;
|
let emptyCount = 0;
|
||||||
const nextGroups = [];
|
const nextGroups = [];
|
||||||
for (const group of lineGroups) {
|
for (const group of lineGroups) {
|
||||||
const toMerge: { [txid: string]: RbfInfo[][] } = {};
|
const toMerge: { [txid: string]: RbfTree[][] } = {};
|
||||||
let emptyInGroup = 0;
|
let emptyInGroup = 0;
|
||||||
let first = true;
|
let first = true;
|
||||||
for (const line of group) {
|
for (const line of group) {
|
||||||
@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
} else {
|
} else {
|
||||||
// substitute duplicates with empty cells
|
// substitute duplicates with empty cells
|
||||||
// (we'll fill these in with connecting lines later)
|
// (we'll fill these in with connecting lines later)
|
||||||
rows[index].unshift(null);
|
rows[index].unshift({ connector: true, replacement: head });
|
||||||
}
|
}
|
||||||
// group the tails of the remaining lines for the next iteration
|
// group the tails of the remaining lines for the next iteration
|
||||||
if (line.length) {
|
if (line.length) {
|
||||||
@ -127,7 +146,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
|
// annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
|
||||||
connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] {
|
connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] {
|
||||||
const rows: TimelineCell[][] = [];
|
const rows: TimelineCell[][] = [];
|
||||||
timelines.forEach((lines, row) => {
|
timelines.forEach((lines, row) => {
|
||||||
rows.push([]);
|
rows.push([]);
|
||||||
@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
let finished = false;
|
let finished = false;
|
||||||
lines.forEach((replacement, column) => {
|
lines.forEach((replacement, column) => {
|
||||||
const cell: TimelineCell = {};
|
const cell: TimelineCell = {};
|
||||||
if (replacement) {
|
if (!isTimelineCell(replacement)) {
|
||||||
cell.replacement = replacement;
|
cell.replacement = replacement as RbfTree;
|
||||||
|
cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf;
|
||||||
}
|
}
|
||||||
rows[row].push(cell);
|
rows[row].push(cell);
|
||||||
if (replacement) {
|
if (!isTimelineCell(replacement)) {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
cell.first = true;
|
cell.first = true;
|
||||||
started = true;
|
started = true;
|
||||||
@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
matched = true;
|
matched = true;
|
||||||
} else if (i === row) {
|
} else if (i === row) {
|
||||||
rows[i][column] = {
|
rows[i][column] = {
|
||||||
connector: 'corner'
|
connector: 'corner',
|
||||||
|
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
|
||||||
};
|
};
|
||||||
} else if (nextCell.connector !== 'corner') {
|
} else if (nextCell.connector !== 'corner') {
|
||||||
rows[i][column] = {
|
rows[i][column] = {
|
||||||
connector: 'pipe'
|
connector: 'pipe',
|
||||||
|
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,12 @@ export class RewardStatsComponent implements OnInit {
|
|||||||
// Or when we receive a newer block, newer than the latest reward stats api call
|
// Or when we receive a newer block, newer than the latest reward stats api call
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((block) => {
|
switchMap((blocks) => {
|
||||||
if (block[0].height <= this.lastBlockHeight) {
|
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||||
|
if (maxHeight <= this.lastBlockHeight) {
|
||||||
return []; // Return an empty stream so the last pipe is not executed
|
return []; // Return an empty stream so the last pipe is not executed
|
||||||
}
|
}
|
||||||
this.lastBlockHeight = block[0].height;
|
this.lastBlockHeight = maxHeight;
|
||||||
return this.apiService.getRewardStats$();
|
return this.apiService.getRewardStats$();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -80,6 +80,9 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}),
|
}),
|
||||||
|
tap((text) => {
|
||||||
|
this.stateService.searchText$.next(text);
|
||||||
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Inpu
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
|
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-start',
|
selector: 'app-start',
|
||||||
@ -55,8 +56,8 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => {
|
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||||
this.blockCount++;
|
this.blockCount = blocks.length;
|
||||||
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
|
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
|
||||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||||
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
|
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
|
||||||
@ -110,9 +111,12 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.subscribe((blocks: any) => {
|
.subscribe((blocks: BlockExtended[]) => {
|
||||||
this.countdown = 0;
|
this.countdown = 0;
|
||||||
const block = blocks[0];
|
const block = blocks[0];
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const sb in specialBlocks) {
|
for (const sb in specialBlocks) {
|
||||||
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
|
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
<div class="container-buttons">
|
<div class="container-buttons">
|
||||||
<app-confirmations
|
<app-confirmations
|
||||||
|
*ngIf="tx"
|
||||||
[chainTip]="latestBlock?.height"
|
[chainTip]="latestBlock?.height"
|
||||||
[height]="tx?.status?.block_height"
|
[height]="tx?.status?.block_height"
|
||||||
[replaced]="replaced"
|
[replaced]="replaced"
|
||||||
@ -306,7 +307,7 @@
|
|||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf]="isLoadingTx && !error">
|
<ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -451,7 +452,7 @@
|
|||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf]="error">
|
<ng-template [ngIf]="error && !loadingCachedTx">
|
||||||
|
|
||||||
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
|
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
|
||||||
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>
|
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
tap
|
tap
|
||||||
} 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, from, throwError } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { CacheService } from '../../services/cache.service';
|
import { CacheService } from '../../services/cache.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -39,6 +39,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
isLoadingTx = true;
|
isLoadingTx = true;
|
||||||
error: any = undefined;
|
error: any = undefined;
|
||||||
errorUnblinded: any = undefined;
|
errorUnblinded: any = undefined;
|
||||||
|
loadingCachedTx = false;
|
||||||
waitingForTransaction = false;
|
waitingForTransaction = false;
|
||||||
latestBlock: BlockExtended;
|
latestBlock: BlockExtended;
|
||||||
transactionTime = -1;
|
transactionTime = -1;
|
||||||
@ -49,10 +50,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
txReplacedSubscription: Subscription;
|
txReplacedSubscription: Subscription;
|
||||||
txRbfInfoSubscription: Subscription;
|
txRbfInfoSubscription: Subscription;
|
||||||
mempoolPositionSubscription: Subscription;
|
mempoolPositionSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
urlFragmentSubscription: Subscription;
|
urlFragmentSubscription: Subscription;
|
||||||
mempoolBlocksSubscription: Subscription;
|
mempoolBlocksSubscription: Subscription;
|
||||||
|
blocksSubscription: Subscription;
|
||||||
fragmentParams: URLSearchParams;
|
fragmentParams: URLSearchParams;
|
||||||
rbfTransaction: undefined | Transaction;
|
rbfTransaction: undefined | Transaction;
|
||||||
replaced: boolean = false;
|
replaced: boolean = false;
|
||||||
@ -131,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||||
|
this.latestBlock = blocks[0];
|
||||||
|
});
|
||||||
|
|
||||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((txId) =>
|
switchMap((txId) =>
|
||||||
@ -199,6 +204,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.fetchCachedTxSubscription = this.fetchCachedTx$
|
this.fetchCachedTxSubscription = this.fetchCachedTx$
|
||||||
.pipe(
|
.pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.loadingCachedTx = true;
|
||||||
|
}),
|
||||||
switchMap((txId) =>
|
switchMap((txId) =>
|
||||||
this.apiService
|
this.apiService
|
||||||
.getRbfCachedTx$(txId)
|
.getRbfCachedTx$(txId)
|
||||||
@ -207,6 +215,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
).subscribe((tx) => {
|
).subscribe((tx) => {
|
||||||
|
this.loadingCachedTx = false;
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -338,6 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||||
this.isLoadingTx = false;
|
this.isLoadingTx = false;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
|
this.loadingCachedTx = false;
|
||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
this.websocketService.startTrackTransaction(tx.txid);
|
this.websocketService.startTrackTransaction(tx.txid);
|
||||||
this.graphExpanded = false;
|
this.graphExpanded = false;
|
||||||
@ -391,9 +401,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||||
this.latestBlock = block;
|
|
||||||
|
|
||||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||||
this.tx.status = {
|
this.tx.status = {
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
@ -409,6 +417,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
|
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
|
||||||
if (!this.tx) {
|
if (!this.tx) {
|
||||||
this.error = new Error();
|
this.error = new Error();
|
||||||
|
this.loadingCachedTx = false;
|
||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
}
|
}
|
||||||
this.rbfTransaction = rbfTransaction;
|
this.rbfTransaction = rbfTransaction;
|
||||||
@ -593,13 +602,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fetchCachedTxSubscription.unsubscribe();
|
this.fetchCachedTxSubscription.unsubscribe();
|
||||||
this.txReplacedSubscription.unsubscribe();
|
this.txReplacedSubscription.unsubscribe();
|
||||||
this.txRbfInfoSubscription.unsubscribe();
|
this.txRbfInfoSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription.unsubscribe();
|
||||||
this.flowPrefSubscription.unsubscribe();
|
this.flowPrefSubscription.unsubscribe();
|
||||||
this.urlFragmentSubscription.unsubscribe();
|
this.urlFragmentSubscription.unsubscribe();
|
||||||
this.mempoolBlocksSubscription.unsubscribe();
|
this.mempoolBlocksSubscription.unsubscribe();
|
||||||
this.mempoolPositionSubscription.unsubscribe();
|
this.mempoolPositionSubscription.unsubscribe();
|
||||||
this.mempoolBlocksSubscription.unsubscribe();
|
this.mempoolBlocksSubscription.unsubscribe();
|
||||||
|
this.blocksSubscription.unsubscribe();
|
||||||
this.leaveTransaction();
|
this.leaveTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||||
|
@ -75,36 +75,31 @@
|
|||||||
<div class="col" style="max-height: 410px">
|
<div class="col" style="max-height: 410px">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<table class="table lastest-blocks-table">
|
<table class="table lastest-replacements-table">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||||
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
|
||||||
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
<tr *ngFor="let replacement of replacements$ | async;">
|
||||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
<td class="table-cell-txid">
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
|
||||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
|
||||||
onError="this.src = '/resources/mining-pools/default.svg'">
|
|
||||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
|
||||||
<td class="table-cell-size">
|
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
|
||||||
<div class="progress">
|
<td class="table-cell-badges">
|
||||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||||
</div>
|
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -175,39 +175,43 @@
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lastest-blocks-table {
|
.lastest-replacements-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
table-layout:fixed;
|
||||||
tr, td, th {
|
tr, td, th {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
padding-top: 0.65rem !important;
|
padding-top: 0.71rem !important;
|
||||||
padding-bottom: 0.7rem !important;
|
padding-bottom: 0.75rem !important;
|
||||||
}
|
}
|
||||||
.table-cell-height {
|
td {
|
||||||
width: 15%;
|
overflow:hidden;
|
||||||
|
width: 25%;
|
||||||
}
|
}
|
||||||
.table-cell-mined {
|
.table-cell-txid {
|
||||||
width: 35%;
|
width: 25%;
|
||||||
text-align: left;
|
text-align: start;
|
||||||
}
|
}
|
||||||
.table-cell-transaction-count {
|
.table-cell-old-fee {
|
||||||
|
width: 25%;
|
||||||
|
text-align: end;
|
||||||
|
|
||||||
|
@media(max-width: 1080px) {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: right;
|
}
|
||||||
|
}
|
||||||
|
.table-cell-new-fee {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
display: table-cell;
|
text-align: end;
|
||||||
}
|
}
|
||||||
.table-cell-size {
|
.table-cell-badges {
|
||||||
display: none;
|
width: 23%;
|
||||||
text-align: center;
|
padding-right: 0;
|
||||||
width: 30%;
|
padding-left: 5px;
|
||||||
@media (min-width: 485px) {
|
text-align: end;
|
||||||
display: table-cell;
|
|
||||||
}
|
.badge {
|
||||||
@media (min-width: 768px) {
|
margin-left: 5px;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, scan, share, switchMap } from 'rxjs/operators';
|
||||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||||
import { ApiService } from '../services/api.service';
|
import { ApiService } from '../services/api.service';
|
||||||
import { StateService } from '../services/state.service';
|
import { StateService } from '../services/state.service';
|
||||||
import { WebsocketService } from '../services/websocket.service';
|
import { WebsocketService } from '../services/websocket.service';
|
||||||
@ -38,8 +38,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
mempoolLoadingStatus$: Observable<number>;
|
mempoolLoadingStatus$: Observable<number>;
|
||||||
vBytesPerSecondLimit = 1667;
|
vBytesPerSecondLimit = 1667;
|
||||||
blocks$: Observable<BlockExtended[]>;
|
|
||||||
transactions$: Observable<TransactionStripped[]>;
|
transactions$: Observable<TransactionStripped[]>;
|
||||||
|
replacements$: Observable<ReplacementInfo[]>;
|
||||||
latestBlockHeight: number;
|
latestBlockHeight: number;
|
||||||
mempoolTransactionsWeightPerSecondData: any;
|
mempoolTransactionsWeightPerSecondData: any;
|
||||||
mempoolStats$: Observable<MempoolStatsData>;
|
mempoolStats$: Observable<MempoolStatsData>;
|
||||||
@ -58,12 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.currencySubscription.unsubscribe();
|
this.currencySubscription.unsubscribe();
|
||||||
|
this.websocketService.stopTrackRbfSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.seoService.resetTitle();
|
this.seoService.resetTitle();
|
||||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||||
|
this.websocketService.startTrackRbfSummary();
|
||||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||||
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -130,30 +132,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.blocks$ = this.stateService.blocks$
|
|
||||||
.pipe(
|
|
||||||
tap(([block]) => {
|
|
||||||
this.latestBlockHeight = block.height;
|
|
||||||
}),
|
|
||||||
scan((acc, [block]) => {
|
|
||||||
if (acc.find((b) => b.height == block.height)) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
acc.unshift(block);
|
|
||||||
acc = acc.slice(0, 6);
|
|
||||||
|
|
||||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
|
||||||
for (const block of acc) {
|
|
||||||
// @ts-ignore: Need to add an extra field for the template
|
|
||||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
|
||||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.transactions$ = this.stateService.transactions$
|
this.transactions$ = this.stateService.transactions$
|
||||||
.pipe(
|
.pipe(
|
||||||
scan((acc, tx) => {
|
scan((acc, tx) => {
|
||||||
@ -166,6 +144,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.replacements$ = this.stateService.rbfLatestSummary$;
|
||||||
|
|
||||||
this.mempoolStats$ = this.stateService.connectionState$
|
this.mempoolStats$ = this.stateService.connectionState$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((state) => state === 2),
|
filter((state) => state === 2),
|
||||||
@ -226,4 +206,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
trackByBlock(index: number, block: BlockExtended) {
|
trackByBlock(index: number, block: BlockExtended) {
|
||||||
return block.height;
|
return block.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkFullRbf(tree: RbfTree): void {
|
||||||
|
let fullRbf = false;
|
||||||
|
for (const replaced of tree.replaces) {
|
||||||
|
if (!replaced.tx.rbf) {
|
||||||
|
fullRbf = true;
|
||||||
|
}
|
||||||
|
replaced.replacedBy = tree.tx;
|
||||||
|
this.checkFullRbf(replaced);
|
||||||
|
}
|
||||||
|
tree.tx.fullRbf = fullRbf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ng-template #noblockconversion>
|
<ng-template #noblockconversion>
|
||||||
<span [class]="colorClass" *ngIf="(conversions$ | async) as conversions">
|
<span [class]="colorClass" *ngIf="(conversions$ | async) as conversions; else noconversion">
|
||||||
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
|
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
|
||||||
</span>
|
</span>
|
||||||
|
<ng-template #noconversion>
|
||||||
|
<span> </span>
|
||||||
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo {
|
|||||||
mined?: boolean;
|
mined?: boolean;
|
||||||
fullRbf: boolean;
|
fullRbf: boolean;
|
||||||
replaces: RbfTree[];
|
replaces: RbfTree[];
|
||||||
|
replacedBy?: RbfTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DifficultyAdjustment {
|
export interface DifficultyAdjustment {
|
||||||
@ -172,13 +173,15 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
rate?: number; // effective fee rate
|
||||||
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbfTransaction extends TransactionStripped {
|
export interface RbfTransaction extends TransactionStripped {
|
||||||
rbf?: boolean;
|
rbf?: boolean;
|
||||||
mined?: boolean,
|
mined?: boolean,
|
||||||
|
fullRbf?: boolean,
|
||||||
}
|
}
|
||||||
export interface MempoolPosition {
|
export interface MempoolPosition {
|
||||||
block: number,
|
block: number,
|
||||||
|
@ -18,6 +18,7 @@ export interface WebsocketResponse {
|
|||||||
txReplaced?: ReplacedTransaction;
|
txReplaced?: ReplacedTransaction;
|
||||||
rbfInfo?: RbfTree;
|
rbfInfo?: RbfTree;
|
||||||
rbfLatest?: RbfTree[];
|
rbfLatest?: RbfTree[];
|
||||||
|
rbfLatestSummary?: ReplacementInfo[];
|
||||||
utxoSpent?: object;
|
utxoSpent?: object;
|
||||||
transactions?: TransactionStripped[];
|
transactions?: TransactionStripped[];
|
||||||
loadingIndicators?: ILoadingIndicators;
|
loadingIndicators?: ILoadingIndicators;
|
||||||
@ -29,6 +30,7 @@ export interface WebsocketResponse {
|
|||||||
'track-asset'?: string;
|
'track-asset'?: string;
|
||||||
'track-mempool-block'?: number;
|
'track-mempool-block'?: number;
|
||||||
'track-rbf'?: string;
|
'track-rbf'?: string;
|
||||||
|
'track-rbf-summary'?: boolean;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'track-bisq-market'?: string;
|
'track-bisq-market'?: string;
|
||||||
'refresh-blocks'?: boolean;
|
'refresh-blocks'?: boolean;
|
||||||
@ -37,6 +39,16 @@ export interface WebsocketResponse {
|
|||||||
export interface ReplacedTransaction extends Transaction {
|
export interface ReplacedTransaction extends Transaction {
|
||||||
txid: string;
|
txid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReplacementInfo {
|
||||||
|
mined: boolean;
|
||||||
|
fullRbf: boolean;
|
||||||
|
txid: string;
|
||||||
|
oldFee: number;
|
||||||
|
oldVsize: number;
|
||||||
|
newFee: number;
|
||||||
|
newVsize: number;
|
||||||
|
}
|
||||||
export interface MempoolBlock {
|
export interface MempoolBlock {
|
||||||
blink?: boolean;
|
blink?: boolean;
|
||||||
height?: number;
|
height?: number;
|
||||||
@ -77,7 +89,7 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md table-col">
|
||||||
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
|
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
.table-col {
|
||||||
|
max-width: calc(100% - 470px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
.table-col {
|
||||||
|
max-width: calc(100% - 470px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
@ -18,10 +23,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-col {
|
|
||||||
max-width: calc(100% - 470px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-col {
|
.map-col {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" *ngIf="!error">
|
<div class="box" *ngIf="!error">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<table class="table table-borderless table-striped table-fixed">
|
<table class="table table-borderless table-striped table-fixed">
|
||||||
@ -59,6 +58,9 @@
|
|||||||
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
|
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
|
||||||
<td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
|
<td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="!node.geolocation" class="d-none d-md-table-row">
|
||||||
|
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -100,11 +102,50 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="node.geolocation && node.featuresBits">
|
||||||
|
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="!node.geolocation && node.featuresBits" class="d-table-row d-md-none">
|
||||||
|
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #featurebits let-bits="bits">
|
||||||
|
<td i18n="lightning.features" class="text-truncate label">Features</td>
|
||||||
|
<td class="d-flex justify-content-between">
|
||||||
|
<span class="text-truncate w-90">{{ bits }}</span>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div class="box mt-2" *ngIf="!error && showFeatures">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5>Raw bits</h5>
|
||||||
|
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
|
||||||
|
</div>
|
||||||
|
<h5>Decoded</h5>
|
||||||
|
<table class="table table-borderless table-striped table-fixed">
|
||||||
|
<thead>
|
||||||
|
<th style="width: 13%">Bit</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th style="width: 25%; text-align: right">Required</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let feature of node.features">
|
||||||
|
<td style="width: 13%">{{ feature.bit }}</td>
|
||||||
|
<td>{{ feature.name }}</td>
|
||||||
|
<td style="width: 25%; text-align: right">{{ feature.is_required }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">
|
<div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">
|
||||||
|
@ -37,7 +37,7 @@ export class NodeComponent implements OnInit {
|
|||||||
liquidityAd: ILiquidityAd;
|
liquidityAd: ILiquidityAd;
|
||||||
tlvRecords: CustomRecord[];
|
tlvRecords: CustomRecord[];
|
||||||
avgChannelDistance$: Observable<number | null>;
|
avgChannelDistance$: Observable<number | null>;
|
||||||
|
showFeatures = false;
|
||||||
kmToMiles = kmToMiles;
|
kmToMiles = kmToMiles;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -164,4 +164,9 @@ export class NodeComponent implements OnInit {
|
|||||||
onLoadingEvent(e) {
|
onLoadingEvent(e) {
|
||||||
this.channelListLoading = e;
|
this.channelListLoading = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleFeatures() {
|
||||||
|
this.showFeatures = !this.showFeatures;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
|
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
|
||||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.liquidity-ranking">Liquidity Ranking</h1>
|
<h1 *ngIf="!widget" class="float-left" i18n="lightning.connectivity-ranking">Connectivity Ranking</h1>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { map, Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||||
|
import { SeoService } from '../../../services/seo.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '../../../services/state.service';
|
||||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||||
import { LightningApiService } from '../../lightning-api.service';
|
import { LightningApiService } from '../../lightning-api.service';
|
||||||
@ -22,6 +23,7 @@ export class TopNodesPerChannels implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private apiService: LightningApiService,
|
private apiService: LightningApiService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private seoService: SeoService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -32,6 +34,8 @@ export class TopNodesPerChannels implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.widget === false) {
|
if (this.widget === false) {
|
||||||
|
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
|
||||||
|
|
||||||
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
|
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
|
||||||
map((ranking) => {
|
map((ranking) => {
|
||||||
for (const i in ranking) {
|
for (const i in ranking) {
|
||||||
|
@ -18,6 +18,7 @@ export class CacheService {
|
|||||||
txCache: { [txid: string]: Transaction } = {};
|
txCache: { [txid: string]: Transaction } = {};
|
||||||
|
|
||||||
network: string;
|
network: string;
|
||||||
|
blockHashCache: { [hash: string]: BlockExtended } = {};
|
||||||
blockCache: { [height: number]: BlockExtended } = {};
|
blockCache: { [height: number]: BlockExtended } = {};
|
||||||
blockLoading: { [height: number]: boolean } = {};
|
blockLoading: { [height: number]: boolean } = {};
|
||||||
copiesInBlockQueue: { [height: number]: number } = {};
|
copiesInBlockQueue: { [height: number]: number } = {};
|
||||||
@ -27,8 +28,10 @@ export class CacheService {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
) {
|
) {
|
||||||
this.stateService.blocks$.subscribe(([block]) => {
|
this.stateService.blocks$.subscribe((blocks) => {
|
||||||
|
for (const block of blocks) {
|
||||||
this.addBlockToCache(block);
|
this.addBlockToCache(block);
|
||||||
|
}
|
||||||
this.clearBlocks();
|
this.clearBlocks();
|
||||||
});
|
});
|
||||||
this.stateService.chainTip$.subscribe((height) => {
|
this.stateService.chainTip$.subscribe((height) => {
|
||||||
@ -56,9 +59,12 @@ export class CacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addBlockToCache(block: BlockExtended) {
|
addBlockToCache(block: BlockExtended) {
|
||||||
|
if (!this.blockHashCache[block.id]) {
|
||||||
|
this.blockHashCache[block.id] = block;
|
||||||
this.blockCache[block.height] = block;
|
this.blockCache[block.height] = block;
|
||||||
this.bumpBlockPriority(block.height);
|
this.bumpBlockPriority(block.height);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadBlock(height) {
|
async loadBlock(height) {
|
||||||
if (!this.blockCache[height] && !this.blockLoading[height]) {
|
if (!this.blockCache[height] && !this.blockLoading[height]) {
|
||||||
@ -105,7 +111,9 @@ export class CacheService {
|
|||||||
} else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
|
} else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
|
||||||
this.bumpBlockPriority(height);
|
this.bumpBlockPriority(height);
|
||||||
} else {
|
} else {
|
||||||
|
const block = this.blockCache[height];
|
||||||
delete this.blockCache[height];
|
delete this.blockCache[height];
|
||||||
|
delete this.blockHashCache[block.id];
|
||||||
delete this.copiesInBlockQueue[height];
|
delete this.copiesInBlockQueue[height];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,6 +121,7 @@ export class CacheService {
|
|||||||
|
|
||||||
// remove all blocks from the cache
|
// remove all blocks from the cache
|
||||||
resetBlockCache() {
|
resetBlockCache() {
|
||||||
|
this.blockHashCache = {};
|
||||||
this.blockCache = {};
|
this.blockCache = {};
|
||||||
this.blockLoading = {};
|
this.blockLoading = {};
|
||||||
this.copiesInBlockQueue = {};
|
this.copiesInBlockQueue = {};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
@ -65,12 +65,12 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressTransactions$(address: string): Observable<Transaction[]> {
|
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
|
let params = new HttpParams();
|
||||||
|
if (txid) {
|
||||||
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
|
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||||
getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> {
|
|
||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAsset$(assetId: string): Observable<Asset> {
|
getAsset$(assetId: string): Observable<Asset> {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||||
import { Transaction } from '../interfaces/electrs.interface';
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
|
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||||
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||||
import { Router, NavigationStart } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { map, scan, shareReplay, tap } from 'rxjs/operators';
|
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
export interface MarkBlockState {
|
export interface MarkBlockState {
|
||||||
@ -90,10 +90,12 @@ export class StateService {
|
|||||||
blockVSize: number;
|
blockVSize: number;
|
||||||
env: Env;
|
env: Env;
|
||||||
latestBlockHeight = -1;
|
latestBlockHeight = -1;
|
||||||
|
blocks: BlockExtended[] = [];
|
||||||
|
|
||||||
networkChanged$ = new ReplaySubject<string>(1);
|
networkChanged$ = new ReplaySubject<string>(1);
|
||||||
lightningChanged$ = new ReplaySubject<boolean>(1);
|
lightningChanged$ = new ReplaySubject<boolean>(1);
|
||||||
blocks$: ReplaySubject<[BlockExtended, string]>;
|
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
|
||||||
|
blocks$: Observable<BlockExtended[]>;
|
||||||
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||||
conversions$ = new ReplaySubject<any>(1);
|
conversions$ = new ReplaySubject<any>(1);
|
||||||
bsqPrice$ = new ReplaySubject<number>(1);
|
bsqPrice$ = new ReplaySubject<number>(1);
|
||||||
@ -102,9 +104,11 @@ export class StateService {
|
|||||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||||
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||||
|
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||||
txRbfInfo$ = new Subject<RbfTree>();
|
txRbfInfo$ = new Subject<RbfTree>();
|
||||||
rbfLatest$ = new Subject<RbfTree[]>();
|
rbfLatest$ = new Subject<RbfTree[]>();
|
||||||
|
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
|
||||||
utxoSpent$ = new Subject<object>();
|
utxoSpent$ = new Subject<object>();
|
||||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||||
mempoolTransactions$ = new Subject<Transaction>();
|
mempoolTransactions$ = new Subject<Transaction>();
|
||||||
@ -126,6 +130,7 @@ export class StateService {
|
|||||||
|
|
||||||
markBlock$ = new BehaviorSubject<MarkBlockState>({});
|
markBlock$ = new BehaviorSubject<MarkBlockState>({});
|
||||||
keyNavigation$ = new Subject<KeyboardEvent>();
|
keyNavigation$ = new Subject<KeyboardEvent>();
|
||||||
|
searchText$ = new BehaviorSubject<string>('');
|
||||||
|
|
||||||
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
||||||
resetScroll$: Subject<boolean> = new Subject<boolean>();
|
resetScroll$: Subject<boolean> = new Subject<boolean>();
|
||||||
@ -167,8 +172,6 @@ export class StateService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
|
|
||||||
|
|
||||||
this.liveMempoolBlockTransactions$ = merge(
|
this.liveMempoolBlockTransactions$ = merge(
|
||||||
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
|
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
|
||||||
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
|
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
|
||||||
@ -198,8 +201,15 @@ export class StateService {
|
|||||||
this.networkChanged$.next(this.env.BASE_MODULE);
|
this.networkChanged$.next(this.env.BASE_MODULE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.networkChanged$.subscribe((network) => {
|
||||||
|
this.transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||||
|
this.blocksSubject$.next([]);
|
||||||
|
});
|
||||||
|
|
||||||
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
||||||
|
|
||||||
|
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
|
||||||
|
|
||||||
const savedTimePreference = this.storageService.getValue('time-preference-ltr');
|
const savedTimePreference = this.storageService.getValue('time-preference-ltr');
|
||||||
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
|
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
|
||||||
// default time direction is right-to-left, unless locale is a RTL language
|
// default time direction is right-to-left, unless locale is a RTL language
|
||||||
@ -336,4 +346,15 @@ export class StateService {
|
|||||||
this.chainTip$.next(height);
|
this.chainTip$.next(height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetBlocks(blocks: BlockExtended[]): void {
|
||||||
|
this.blocks = blocks.reverse();
|
||||||
|
this.blocksSubject$.next(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
addBlock(block: BlockExtended): void {
|
||||||
|
this.blocks.unshift(block);
|
||||||
|
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
this.blocksSubject$.next(this.blocks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||||
import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface';
|
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { Transaction } from '../interfaces/electrs.interface';
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
||||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
const OFFLINE_RETRY_AFTER_MS = 2000;
|
const OFFLINE_RETRY_AFTER_MS = 2000;
|
||||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
|
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
|
||||||
@ -29,6 +29,7 @@ export class WebsocketService {
|
|||||||
private trackingTxId: string;
|
private trackingTxId: string;
|
||||||
private isTrackingMempoolBlock = false;
|
private isTrackingMempoolBlock = false;
|
||||||
private isTrackingRbf = false;
|
private isTrackingRbf = false;
|
||||||
|
private isTrackingRbfSummary = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private latestGitCommit = '';
|
private latestGitCommit = '';
|
||||||
private onlineCheckTimeout: number;
|
private onlineCheckTimeout: number;
|
||||||
@ -40,6 +41,7 @@ export class WebsocketService {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private transferState: TransferState,
|
private transferState: TransferState,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
if (!this.stateService.isBrowser) {
|
if (!this.stateService.isBrowser) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -184,6 +186,16 @@ export class WebsocketService {
|
|||||||
this.isTrackingRbf = false;
|
this.isTrackingRbf = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startTrackRbfSummary() {
|
||||||
|
this.websocketSubject.next({ 'track-rbf-summary': true });
|
||||||
|
this.isTrackingRbfSummary = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTrackRbfSummary() {
|
||||||
|
this.websocketSubject.next({ 'track-rbf-summary': false });
|
||||||
|
this.isTrackingRbfSummary = false;
|
||||||
|
}
|
||||||
|
|
||||||
startTrackBisqMarket(market: string) {
|
startTrackBisqMarket(market: string) {
|
||||||
this.websocketSubject.next({ 'track-bisq-market': market });
|
this.websocketSubject.next({ 'track-bisq-market': market });
|
||||||
}
|
}
|
||||||
@ -239,13 +251,8 @@ export class WebsocketService {
|
|||||||
|
|
||||||
if (response.blocks && response.blocks.length) {
|
if (response.blocks && response.blocks.length) {
|
||||||
const blocks = response.blocks;
|
const blocks = response.blocks;
|
||||||
let maxHeight = 0;
|
this.stateService.resetBlocks(blocks);
|
||||||
blocks.forEach((block: BlockExtended) => {
|
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
|
||||||
if (block.height > this.stateService.latestBlockHeight) {
|
|
||||||
maxHeight = Math.max(maxHeight, block.height);
|
|
||||||
this.stateService.blocks$.next([block, '']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.stateService.updateChainTip(maxHeight);
|
this.stateService.updateChainTip(maxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +267,8 @@ export class WebsocketService {
|
|||||||
if (response.block) {
|
if (response.block) {
|
||||||
if (response.block.height === this.stateService.latestBlockHeight + 1) {
|
if (response.block.height === this.stateService.latestBlockHeight + 1) {
|
||||||
this.stateService.updateChainTip(response.block.height);
|
this.stateService.updateChainTip(response.block.height);
|
||||||
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
this.stateService.addBlock(response.block);
|
||||||
|
this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
|
||||||
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
||||||
reinitBlocks = true;
|
reinitBlocks = true;
|
||||||
}
|
}
|
||||||
@ -286,6 +294,10 @@ export class WebsocketService {
|
|||||||
this.stateService.rbfLatest$.next(response.rbfLatest);
|
this.stateService.rbfLatest$.next(response.rbfLatest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.rbfLatestSummary) {
|
||||||
|
this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.txReplaced) {
|
if (response.txReplaced) {
|
||||||
this.stateService.txReplaced$.next(response.txReplaced);
|
this.stateService.txReplaced$.next(response.txReplaced);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user