Merge branch 'master' into mononaut/atomic-cpfp
This commit is contained in:
commit
f80257c5ea
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.16.0", "18.14.1"]
|
node: ["16", "17", "18"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -55,7 +55,7 @@ jobs:
|
|||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["16.16.0", "18.14.1"]
|
node: ["16", "17", "18"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
@ -399,9 +399,13 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
private async getBlockAuditSummary(req: Request, res: Response) {
|
private async getBlockAuditSummary(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
|
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
|
||||||
|
if (auditSummary) {
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transactions);
|
res.json(auditSummary);
|
||||||
|
} else {
|
||||||
|
return res.status(404).send(`audit not available`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
@ -730,6 +730,11 @@ class Blocks {
|
|||||||
this.currentDifficulty = block.difficulty;
|
this.currentDifficulty = block.difficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for pending async callbacks to finish
|
||||||
|
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
|
||||||
|
await Promise.all(callbackPromises);
|
||||||
|
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
this.blocks.push(blockExtended);
|
this.blocks.push(blockExtended);
|
||||||
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
@ -746,11 +751,6 @@ class Blocks {
|
|||||||
diskCache.$saveCacheToDisk();
|
diskCache.$saveCacheToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for pending async callbacks to finish
|
|
||||||
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
|
|
||||||
await Promise.all(callbackPromises);
|
|
||||||
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
|
|
||||||
|
|
||||||
handledBlocks++;
|
handledBlocks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1007,19 +1007,11 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
public async $getBlockAuditSummary(hash: string): Promise<any> {
|
||||||
let summary;
|
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
summary = await BlocksAuditsRepository.$getBlockAudit(hash);
|
return BlocksAuditsRepository.$getBlockAudit(hash);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to non-audited transaction summary
|
|
||||||
if (!summary?.transactions?.length) {
|
|
||||||
const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
|
|
||||||
summary = {
|
|
||||||
transactions: strippedTransactions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return summary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLastDifficultyAdjustmentTime(): number {
|
public getLastDifficultyAdjustmentTime(): number {
|
||||||
|
@ -211,7 +211,7 @@ class StatisticsApi {
|
|||||||
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
|
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
|
||||||
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
|
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
|
||||||
FROM statistics \
|
FROM statistics \
|
||||||
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
|
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
|
||||||
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||||
ORDER BY statistics.added DESC;`;
|
ORDER BY statistics.added DESC;`;
|
||||||
}
|
}
|
||||||
@ -259,7 +259,7 @@ class StatisticsApi {
|
|||||||
vsize_1800,
|
vsize_1800,
|
||||||
vsize_2000 \
|
vsize_2000 \
|
||||||
FROM statistics \
|
FROM statistics \
|
||||||
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
|
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
|
||||||
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||||
ORDER BY statistics.added DESC;`;
|
ORDER BY statistics.added DESC;`;
|
||||||
}
|
}
|
||||||
@ -386,6 +386,17 @@ class StatisticsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $listAll(): Promise<OptimizedStatistic[]> {
|
||||||
|
try {
|
||||||
|
const query = this.getQueryForDays(43200, 'all'); // 12h interval
|
||||||
|
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
|
||||||
|
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$listAll() error' + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
|
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
|
||||||
return statistic.map((s) => {
|
return statistic.map((s) => {
|
||||||
return {
|
return {
|
||||||
|
@ -15,10 +15,11 @@ class StatisticsRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/all', this.$getStatisticsByTime.bind(this, 'all'))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
|
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y' | 'all', req: Request, res: Response) {
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
@ -26,10 +27,6 @@ class StatisticsRoutes {
|
|||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
switch (time as string) {
|
switch (time as string) {
|
||||||
case '2h':
|
|
||||||
result = await statisticsApi.$list2H();
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
|
||||||
break;
|
|
||||||
case '24h':
|
case '24h':
|
||||||
result = await statisticsApi.$list24H();
|
result = await statisticsApi.$list24H();
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
@ -58,8 +55,13 @@ class StatisticsRoutes {
|
|||||||
case '4y':
|
case '4y':
|
||||||
result = await statisticsApi.$list4Y();
|
result = await statisticsApi.$list4Y();
|
||||||
break;
|
break;
|
||||||
|
case 'all':
|
||||||
|
result = await statisticsApi.$listAll();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result = await statisticsApi.$list2H();
|
result = await statisticsApi.$list2H();
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -22,6 +22,14 @@ import { deepClone } from '../utils/clone';
|
|||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
|
|
||||||
|
// valid 'want' subscriptions
|
||||||
|
const wantable = [
|
||||||
|
'blocks',
|
||||||
|
'mempool-blocks',
|
||||||
|
'live-2h-chart',
|
||||||
|
'stats',
|
||||||
|
];
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
private extraInitProperties = {};
|
private extraInitProperties = {};
|
||||||
@ -30,7 +38,7 @@ class WebsocketHandler {
|
|||||||
private numConnected = 0;
|
private numConnected = 0;
|
||||||
private numDisconnected = 0;
|
private numDisconnected = 0;
|
||||||
|
|
||||||
private initData: { [key: string]: string } = {};
|
private socketData: { [key: string]: string } = {};
|
||||||
private serializedInitData: string = '{}';
|
private serializedInitData: string = '{}';
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -39,28 +47,28 @@ class WebsocketHandler {
|
|||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtraInitProperties(property: string, value: any) {
|
setExtraInitData(property: string, value: any) {
|
||||||
this.extraInitProperties[property] = value;
|
this.extraInitProperties[property] = value;
|
||||||
this.setInitDataFields(this.extraInitProperties);
|
this.updateSocketDataFields(this.extraInitProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setInitDataFields(data: { [property: string]: any }): void {
|
private updateSocketDataFields(data: { [property: string]: any }): void {
|
||||||
for (const property of Object.keys(data)) {
|
for (const property of Object.keys(data)) {
|
||||||
if (data[property] != null) {
|
if (data[property] != null) {
|
||||||
this.initData[property] = JSON.stringify(data[property]);
|
this.socketData[property] = JSON.stringify(data[property]);
|
||||||
} else {
|
} else {
|
||||||
delete this.initData[property];
|
delete this.socketData[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.serializedInitData = '{'
|
this.serializedInitData = '{'
|
||||||
+ Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
|
+ Object.keys(this.socketData).map(key => `"${key}": ${this.socketData[key]}`).join(', ')
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateInitData(): void {
|
private updateSocketData(): void {
|
||||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
this.setInitDataFields({
|
this.updateSocketDataFields({
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
'blocks': _blocks,
|
'blocks': _blocks,
|
||||||
@ -94,11 +102,33 @@ class WebsocketHandler {
|
|||||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||||
const response = {};
|
const response = {};
|
||||||
|
|
||||||
if (parsedMessage.action === 'want') {
|
const wantNow = {};
|
||||||
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
if (parsedMessage && parsedMessage.action === 'want' && Array.isArray(parsedMessage.data)) {
|
||||||
client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
|
for (const sub of wantable) {
|
||||||
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
const key = `want-${sub}`;
|
||||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
const wants = parsedMessage.data.includes(sub);
|
||||||
|
if (wants && client['wants'] && !client[key]) {
|
||||||
|
wantNow[key] = true;
|
||||||
|
}
|
||||||
|
client[key] = wants;
|
||||||
|
}
|
||||||
|
client['wants'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send initial data when a client first starts a subscription
|
||||||
|
if (wantNow['want-blocks'] || (parsedMessage && parsedMessage['refresh-blocks'])) {
|
||||||
|
response['blocks'] = this.socketData['blocks'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantNow['want-mempool-blocks']) {
|
||||||
|
response['mempool-blocks'] = this.socketData['mempool-blocks'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantNow['want-stats']) {
|
||||||
|
response['mempoolInfo'] = this.socketData['mempoolInfo'];
|
||||||
|
response['vBytesPerSecond'] = this.socketData['vBytesPerSecond'];
|
||||||
|
response['fees'] = this.socketData['fees'];
|
||||||
|
response['da'] = this.socketData['da'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-tx']) {
|
if (parsedMessage && parsedMessage['track-tx']) {
|
||||||
@ -109,21 +139,21 @@ class WebsocketHandler {
|
|||||||
if (parsedMessage['watch-mempool']) {
|
if (parsedMessage['watch-mempool']) {
|
||||||
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
|
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
|
||||||
if (rbfCacheTxid) {
|
if (rbfCacheTxid) {
|
||||||
response['txReplaced'] = {
|
response['txReplaced'] = JSON.stringify({
|
||||||
txid: rbfCacheTxid,
|
txid: rbfCacheTxid,
|
||||||
};
|
});
|
||||||
client['track-tx'] = null;
|
client['track-tx'] = null;
|
||||||
} else {
|
} else {
|
||||||
// It might have appeared before we had the time to start watching for it
|
// It might have appeared before we had the time to start watching for it
|
||||||
const tx = memPool.getMempool()[trackTxid];
|
const tx = memPool.getMempool()[trackTxid];
|
||||||
if (tx) {
|
if (tx) {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
response['tx'] = tx;
|
response['tx'] = JSON.stringify(tx);
|
||||||
} else {
|
} else {
|
||||||
// tx.prevout is missing from transactions when in bitcoind mode
|
// tx.prevout is missing from transactions when in bitcoind mode
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = JSON.stringify(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -131,7 +161,7 @@ class WebsocketHandler {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = JSON.stringify(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||||
@ -141,10 +171,10 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
const tx = memPool.getMempool()[trackTxid];
|
const tx = memPool.getMempool()[trackTxid];
|
||||||
if (tx && tx.position) {
|
if (tx && tx.position) {
|
||||||
response['txPosition'] = {
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position: tx.position,
|
position: tx.position,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client['track-tx'] = null;
|
client['track-tx'] = null;
|
||||||
@ -177,10 +207,10 @@ class WebsocketHandler {
|
|||||||
const index = parsedMessage['track-mempool-block'];
|
const index = parsedMessage['track-mempool-block'];
|
||||||
client['track-mempool-block'] = index;
|
client['track-mempool-block'] = index;
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
response['projected-block-transactions'] = {
|
response['projected-block-transactions'] = JSON.stringify({
|
||||||
index: index,
|
index: index,
|
||||||
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
||||||
};
|
});
|
||||||
} else {
|
} else {
|
||||||
client['track-mempool-block'] = null;
|
client['track-mempool-block'] = null;
|
||||||
}
|
}
|
||||||
@ -189,23 +219,24 @@ class WebsocketHandler {
|
|||||||
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
|
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
|
||||||
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
|
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
|
||||||
client['track-rbf'] = parsedMessage['track-rbf'];
|
client['track-rbf'] = parsedMessage['track-rbf'];
|
||||||
|
response['rbfLatest'] = JSON.stringify(rbfCache.getRbfTrees(parsedMessage['track-rbf'] === 'fullRbf'));
|
||||||
} else {
|
} else {
|
||||||
client['track-rbf'] = false;
|
client['track-rbf'] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
if (!this.initData['blocks']?.length || !this.initData['da']) {
|
if (!this.socketData['blocks']?.length || !this.socketData['da']) {
|
||||||
this.updateInitData();
|
this.updateSocketData();
|
||||||
}
|
}
|
||||||
if (!this.initData['blocks']?.length) {
|
if (!this.socketData['blocks']?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.send(this.serializedInitData);
|
client.send(this.serializedInitData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'ping') {
|
if (parsedMessage.action === 'ping') {
|
||||||
response['pong'] = true;
|
response['pong'] = JSON.stringify(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
|
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
|
||||||
@ -221,7 +252,8 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(JSON.stringify(response));
|
const serializedResponse = this.serializeResponse(response);
|
||||||
|
client.send(serializedResponse);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -250,7 +282,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setInitDataFields({ 'loadingIndicators': indicators });
|
this.updateSocketDataFields({ 'loadingIndicators': indicators });
|
||||||
|
|
||||||
const response = JSON.stringify({ loadingIndicators: indicators });
|
const response = JSON.stringify({ loadingIndicators: indicators });
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
@ -266,7 +298,7 @@ class WebsocketHandler {
|
|||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setInitDataFields({ 'conversions': conversionRates });
|
this.updateSocketDataFields({ 'conversions': conversionRates });
|
||||||
|
|
||||||
const response = JSON.stringify({ conversions: conversionRates });
|
const response = JSON.stringify({ conversions: conversionRates });
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
@ -336,11 +368,21 @@ class WebsocketHandler {
|
|||||||
memPool.addToSpendMap(newTransactions);
|
memPool.addToSpendMap(newTransactions);
|
||||||
const recommendedFees = feeApi.getRecommendedFee();
|
const recommendedFees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
|
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||||
|
|
||||||
// update init data
|
// update init data
|
||||||
this.updateInitData();
|
this.updateSocketDataFields({
|
||||||
|
'mempoolInfo': mempoolInfo,
|
||||||
|
'vBytesPerSecond': vBytesPerSecond,
|
||||||
|
'mempool-blocks': mBlocks,
|
||||||
|
'transactions': latestTransactions,
|
||||||
|
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||||
|
'da': da?.previousTime ? da : undefined,
|
||||||
|
'fees': recommendedFees,
|
||||||
|
});
|
||||||
|
|
||||||
// cache serialized objects to avoid stringify-ing the same thing for every client
|
// cache serialized objects to avoid stringify-ing the same thing for every client
|
||||||
const responseCache = { ...this.initData };
|
const responseCache = { ...this.socketData };
|
||||||
function getCachedResponse(key: string, data): string {
|
function getCachedResponse(key: string, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
responseCache[key] = JSON.stringify(data);
|
responseCache[key] = JSON.stringify(data);
|
||||||
@ -371,8 +413,6 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
|
||||||
|
|
||||||
this.wss.clients.forEach(async (client) => {
|
this.wss.clients.forEach(async (client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@ -490,7 +530,7 @@ class WebsocketHandler {
|
|||||||
if (rbfReplacedBy) {
|
if (rbfReplacedBy) {
|
||||||
response['rbfTransaction'] = JSON.stringify({
|
response['rbfTransaction'] = JSON.stringify({
|
||||||
txid: rbfReplacedBy,
|
txid: rbfReplacedBy,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rbfChange = rbfChanges.map[client['track-tx']];
|
const rbfChange = rbfChanges.map[client['track-tx']];
|
||||||
@ -524,9 +564,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
const serializedResponse = '{'
|
const serializedResponse = this.serializeResponse(response);
|
||||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
|
||||||
+ '}';
|
|
||||||
client.send(serializedResponse);
|
client.send(serializedResponse);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -633,11 +671,19 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const fees = feeApi.getRecommendedFee();
|
const fees = feeApi.getRecommendedFee();
|
||||||
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
|
|
||||||
// update init data
|
// update init data
|
||||||
this.updateInitData();
|
this.updateSocketDataFields({
|
||||||
|
'mempoolInfo': mempoolInfo,
|
||||||
|
'blocks': [...blocks.getBlocks(), block].slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT),
|
||||||
|
'mempool-blocks': mBlocks,
|
||||||
|
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||||
|
'da': da?.previousTime ? da : undefined,
|
||||||
|
'fees': fees,
|
||||||
|
});
|
||||||
|
|
||||||
const responseCache = { ...this.initData };
|
const responseCache = { ...this.socketData };
|
||||||
function getCachedResponse(key, data): string {
|
function getCachedResponse(key, data): string {
|
||||||
if (!responseCache[key]) {
|
if (!responseCache[key]) {
|
||||||
responseCache[key] = JSON.stringify(data);
|
responseCache[key] = JSON.stringify(data);
|
||||||
@ -645,23 +691,27 @@ class WebsocketHandler {
|
|||||||
return responseCache[key];
|
return responseCache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
|
||||||
|
|
||||||
this.wss.clients.forEach((client) => {
|
this.wss.clients.forEach((client) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client['want-blocks']) {
|
const response = {};
|
||||||
return;
|
|
||||||
|
if (client['want-blocks']) {
|
||||||
|
response['block'] = getCachedResponse('block', block);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {};
|
if (client['want-stats']) {
|
||||||
response['block'] = getCachedResponse('block', block);
|
|
||||||
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
|
||||||
response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
|
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
|
||||||
response['fees'] = getCachedResponse('fees', fees);
|
response['fees'] = getCachedResponse('fees', fees);
|
||||||
|
|
||||||
|
if (da?.previousTime) {
|
||||||
|
response['da'] = getCachedResponse('da', da);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mBlocks && client['want-mempool-blocks']) {
|
if (mBlocks && client['want-mempool-blocks']) {
|
||||||
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
|
||||||
}
|
}
|
||||||
@ -755,11 +805,19 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializedResponse = '{'
|
if (Object.keys(response).length) {
|
||||||
|
const serializedResponse = this.serializeResponse(response);
|
||||||
|
client.send(serializedResponse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes a dictionary of JSON serialized values
|
||||||
|
// and zips it together into a valid JSON object
|
||||||
|
private serializeResponse(response): string {
|
||||||
|
return '{'
|
||||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||||
+ '}';
|
+ '}';
|
||||||
client.send(serializedResponse);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private printLogs(): void {
|
private printLogs(): void {
|
||||||
|
@ -150,7 +150,7 @@ class Server {
|
|||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
bisq.startBisqService();
|
bisq.startBisqService();
|
||||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
|
||||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||||
bisqMarkets.startBisqService();
|
bisqMarkets.startBisqService();
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,6 @@ class BlocksAuditRepositories {
|
|||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
|
||||||
blocks.weight, blocks.tx_count,
|
blocks.weight, blocks.tx_count,
|
||||||
transactions,
|
|
||||||
template,
|
template,
|
||||||
missing_txs as missingTxs,
|
missing_txs as missingTxs,
|
||||||
added_txs as addedTxs,
|
added_txs as addedTxs,
|
||||||
@ -76,7 +75,6 @@ class BlocksAuditRepositories {
|
|||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks ON blocks.hash = blocks_audits.hash
|
JOIN blocks ON blocks.hash = blocks_audits.hash
|
||||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||||
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
|
||||||
WHERE blocks_audits.hash = "${hash}"
|
WHERE blocks_audits.hash = "${hash}"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -85,13 +83,10 @@ class BlocksAuditRepositories {
|
|||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||||
rows[0].transactions = JSON.parse(rows[0].transactions);
|
|
||||||
rows[0].template = JSON.parse(rows[0].template);
|
rows[0].template = JSON.parse(rows[0].template);
|
||||||
|
|
||||||
if (rows[0].transactions.length) {
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
@ -173,6 +173,21 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://www.luminex.io" target="_blank" title="Luminex">
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.lum-cls-1 {
|
||||||
|
fill: #f2ea25;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
|
||||||
|
<path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
|
||||||
|
<rect class="lum-cls-1" width="60.69" height="372.67"/>
|
||||||
|
</svg>
|
||||||
|
<span>Luminex</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -205,7 +220,7 @@
|
|||||||
<img class="image" src="/resources/profile/mynodebtc.png" />
|
<img class="image" src="/resources/profile/mynodebtc.png" />
|
||||||
<span>myNode</span>
|
<span>myNode</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
|
<a href="https://code.samourai.io/ronindojo/RoninDojo" target="_blank" title="RoninDojo">
|
||||||
<img class="image" src="/resources/profile/ronindojo.png" />
|
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||||
<span>RoninDojo</span>
|
<span>RoninDojo</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
||||||
>{{ blockAudit?.matchRate }}%</span>
|
>{{ blockAudit?.matchRate }}%</span>
|
||||||
<ng-template #nullHealth>
|
<ng-template #nullHealth>
|
||||||
<ng-container *ngIf="!isLoadingAudit; else loadingHealth">
|
<ng-container *ngIf="!isLoadingOverview; else loadingHealth">
|
||||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
|
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith } from 'rxjs/operators';
|
||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
|
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
@ -44,7 +44,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
strippedTransactions: TransactionStripped[];
|
strippedTransactions: TransactionStripped[];
|
||||||
overviewTransitionDirection: string;
|
overviewTransitionDirection: string;
|
||||||
isLoadingOverview = true;
|
isLoadingOverview = true;
|
||||||
isLoadingAudit = true;
|
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
@ -281,57 +280,35 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.auditSupported) {
|
|
||||||
this.overviewSubscription = block$.pipe(
|
this.overviewSubscription = block$.pipe(
|
||||||
startWith(null),
|
switchMap((block) => {
|
||||||
pairwise(),
|
return forkJoin([
|
||||||
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
|
this.apiService.getStrippedBlockTransactions$(block.id)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.overviewError = err;
|
this.overviewError = err;
|
||||||
return of([]);
|
return of(null);
|
||||||
}),
|
})
|
||||||
switchMap((transactions) => {
|
),
|
||||||
if (prevBlock) {
|
!this.isAuditAvailableFromBlockHeight(block.height) ? of(null) : this.apiService.getBlockAudit$(block.id)
|
||||||
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
|
.pipe(
|
||||||
} else {
|
catchError((err) => {
|
||||||
return of({ transactions, direction: 'down' });
|
this.overviewError = err;
|
||||||
}
|
return of(null);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
]);
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
|
.subscribe(([transactions, blockAudit]) => {
|
||||||
|
if (transactions) {
|
||||||
this.strippedTransactions = transactions;
|
this.strippedTransactions = transactions;
|
||||||
this.isLoadingOverview = false;
|
} else {
|
||||||
this.setupBlockGraphs();
|
this.strippedTransactions = [];
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
this.error = error;
|
|
||||||
this.isLoadingOverview = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.auditSupported) {
|
|
||||||
this.auditSubscription = block$.pipe(
|
|
||||||
startWith(null),
|
|
||||||
pairwise(),
|
|
||||||
switchMap(([prevBlock, block]) => {
|
|
||||||
this.isLoadingAudit = true;
|
|
||||||
this.blockAudit = null;
|
this.blockAudit = null;
|
||||||
return this.apiService.getBlockAudit$(block.id)
|
if (transactions && blockAudit) {
|
||||||
.pipe(
|
|
||||||
catchError((err) => {
|
|
||||||
this.overviewError = err;
|
|
||||||
this.isLoadingAudit = false;
|
|
||||||
return of([]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
filter((response) => response != null),
|
|
||||||
map((response) => {
|
|
||||||
const blockAudit = response.body;
|
|
||||||
const inTemplate = {};
|
const inTemplate = {};
|
||||||
const inBlock = {};
|
const inBlock = {};
|
||||||
const isAdded = {};
|
const isAdded = {};
|
||||||
@ -347,7 +324,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const tx of blockAudit.template) {
|
for (const tx of blockAudit.template) {
|
||||||
inTemplate[tx.txid] = true;
|
inTemplate[tx.txid] = true;
|
||||||
}
|
}
|
||||||
for (const tx of blockAudit.transactions) {
|
for (const tx of transactions) {
|
||||||
inBlock[tx.txid] = true;
|
inBlock[tx.txid] = true;
|
||||||
}
|
}
|
||||||
for (const txid of blockAudit.addedTxs) {
|
for (const txid of blockAudit.addedTxs) {
|
||||||
@ -375,7 +352,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.numMissing++;
|
this.numMissing++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
for (const [index, tx] of transactions.entries()) {
|
||||||
tx.context = 'actual';
|
tx.context = 'actual';
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
tx.status = null;
|
tx.status = null;
|
||||||
@ -389,36 +366,26 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.numUnexpected++;
|
this.numUnexpected++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const tx of blockAudit.transactions) {
|
for (const tx of transactions) {
|
||||||
inBlock[tx.txid] = true;
|
inBlock[tx.txid] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
|
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - this.block.extras.totalFees) / blockAudit.expectedFees : 0;
|
||||||
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
|
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block.weight) / blockAudit.expectedWeight : 0;
|
||||||
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
|
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block.tx_count) / blockAudit.template.length : 0;
|
||||||
|
this.blockAudit = blockAudit;
|
||||||
this.setAuditAvailable(true);
|
this.setAuditAvailable(true);
|
||||||
} else {
|
} else {
|
||||||
this.setAuditAvailable(false);
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
return blockAudit;
|
} else {
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
console.log(err);
|
|
||||||
this.error = err;
|
|
||||||
this.isLoadingOverview = false;
|
|
||||||
this.isLoadingAudit = false;
|
|
||||||
this.setAuditAvailable(false);
|
this.setAuditAvailable(false);
|
||||||
return of(null);
|
|
||||||
}),
|
|
||||||
).subscribe((blockAudit) => {
|
|
||||||
this.blockAudit = blockAudit;
|
|
||||||
this.setupBlockGraphs();
|
|
||||||
this.isLoadingOverview = false;
|
|
||||||
this.isLoadingAudit = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
});
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
@ -652,25 +619,32 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
||||||
if (!this.auditSupported) {
|
if (!this.isAuditAvailableFromBlockHeight(blockHeight)) {
|
||||||
this.setAuditAvailable(false);
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
|
||||||
|
if (!this.auditSupported) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
switch (this.stateService.network) {
|
switch (this.stateService.network) {
|
||||||
case 'testnet':
|
case 'testnet':
|
||||||
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.setAuditAvailable(false);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinBlockFee(block: BlockExtended): number {
|
getMinBlockFee(block: BlockExtended): number {
|
||||||
|
@ -143,6 +143,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
||||||
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
||||||
|
|
||||||
|
this.now = Date.now();
|
||||||
|
|
||||||
this.updateMempoolBlockStyles();
|
this.updateMempoolBlockStyles();
|
||||||
this.calculateTransactionPosition();
|
this.calculateTransactionPosition();
|
||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
@ -152,7 +154,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((da) => {
|
map((da) => {
|
||||||
this.now = new Date().getTime();
|
this.now = Date.now();
|
||||||
|
this.cd.markForCheck();
|
||||||
return da;
|
return da;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
<div class="btn-toggle-rows" name="radioBasic">
|
||||||
|
<div class="btn-group btn-group-toggle">
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
|
||||||
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
|
<input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
|
||||||
(LIVE)
|
(LIVE)
|
||||||
@ -38,6 +39,8 @@
|
|||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||||
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
|
<input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-toggle">
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
|
||||||
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
|
<input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
|
||||||
</label>
|
</label>
|
||||||
@ -53,6 +56,10 @@
|
|||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
|
||||||
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
|
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
|
||||||
</label>
|
</label>
|
||||||
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
|
||||||
|
<input type="radio" [value]="'all'" [routerLink]="['/graphs' | relativeUrl]" fragment="all" formControlName="dateSpan"><span i18n="all">All</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-buttons">
|
<div class="small-buttons">
|
||||||
<div ngbDropdown #myDrop="ngbDropdown">
|
<div ngbDropdown #myDrop="ngbDropdown">
|
||||||
|
@ -53,17 +53,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formRadioGroup.mining {
|
.formRadioGroup.mining {
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -100px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formRadioGroup.no-menu {
|
.formRadioGroup.no-menu {
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 1035px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -33px;
|
top: -33px;
|
||||||
}
|
}
|
||||||
@ -183,3 +183,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-toggle-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 500px) {
|
||||||
|
.btn-group:first-child > .btn:last-child {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.btn-group:last-child > .btn:first-child {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 499px) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.btn-group:first-child > .btn:first-child {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.btn-group:first-child > .btn:last-child {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.btn-group:last-child > .btn:first-child {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
.btn-group:last-child > .btn:last-child {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,8 +72,10 @@ export class StatisticsComponent implements OnInit {
|
|||||||
this.route
|
this.route
|
||||||
.fragment
|
.fragment
|
||||||
.subscribe((fragment) => {
|
.subscribe((fragment) => {
|
||||||
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) {
|
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y', 'all'].indexOf(fragment) > -1) {
|
||||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||||
|
} else {
|
||||||
|
this.radioGroupForm.controls.dateSpan.setValue('2h', { emitEvent: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,7 +116,12 @@ export class StatisticsComponent implements OnInit {
|
|||||||
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
|
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
|
||||||
return this.apiService.list3YStatistics$();
|
return this.apiService.list3YStatistics$();
|
||||||
}
|
}
|
||||||
|
if (this.radioGroupForm.controls.dateSpan.value === '4y') {
|
||||||
return this.apiService.list4YStatistics$();
|
return this.apiService.list4YStatistics$();
|
||||||
|
}
|
||||||
|
if (this.radioGroupForm.controls.dateSpan.value === 'all') {
|
||||||
|
return this.apiService.listAllTimeStatistics$();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe((mempoolStats: any) => {
|
.subscribe((mempoolStats: any) => {
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #timeEstimateDefault>
|
<ng-template #timeEstimateDefault>
|
||||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * this.mempoolPosition.block) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
|
|||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition } from '../../interfaces/node-api.interface';
|
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
|
||||||
import { LiquidUnblinding } from './liquid-ublinding';
|
import { LiquidUnblinding } from './liquid-ublinding';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { Price, PriceService } from '../../services/price.service';
|
import { Price, PriceService } from '../../services/price.service';
|
||||||
@ -65,7 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
isCached: boolean = false;
|
isCached: boolean = false;
|
||||||
now = Date.now();
|
now = Date.now();
|
||||||
timeAvg$: Observable<number>;
|
da$: Observable<DifficultyAdjustment>;
|
||||||
liquidUnblinding = new LiquidUnblinding();
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
inputIndex: number;
|
inputIndex: number;
|
||||||
outputIndex: number;
|
outputIndex: number;
|
||||||
@ -117,10 +117,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.setFlowEnabled();
|
this.setFlowEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.timeAvg$ = timer(0, 1000)
|
this.da$ = this.stateService.difficultyAdjustment$.pipe(
|
||||||
.pipe(
|
tap(() => {
|
||||||
switchMap(() => this.stateService.difficultyAdjustment$),
|
this.now = Date.now();
|
||||||
map((da) => da.timeAvg)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||||
@ -236,6 +236,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
||||||
|
this.now = Date.now();
|
||||||
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
||||||
this.mempoolPosition = txPosition.position;
|
this.mempoolPosition = txPosition.position;
|
||||||
if (this.tx && !this.tx.status.confirmed) {
|
if (this.tx && !this.tx.status.confirmed) {
|
||||||
@ -434,12 +435,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
|
||||||
|
this.now = Date.now();
|
||||||
|
|
||||||
if (!this.tx || this.mempoolPosition) {
|
if (!this.tx || this.mempoolPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.now = Date.now();
|
|
||||||
|
|
||||||
const txFeePerVSize =
|
const txFeePerVSize =
|
||||||
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
||||||
|
|
||||||
|
@ -153,6 +153,8 @@ export interface BlockExtended extends Block {
|
|||||||
export interface BlockAudit extends BlockExtended {
|
export interface BlockAudit extends BlockExtended {
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
|
freshTxs: string[],
|
||||||
|
sigopTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees: number,
|
expectedFees: number,
|
||||||
expectedWeight: number,
|
expectedWeight: number,
|
||||||
@ -169,6 +171,7 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbfTransaction extends TransactionStripped {
|
interface RbfTransaction extends TransactionStripped {
|
||||||
|
@ -31,6 +31,7 @@ export interface WebsocketResponse {
|
|||||||
'track-rbf'?: string;
|
'track-rbf'?: string;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'track-bisq-market'?: string;
|
'track-bisq-market'?: string;
|
||||||
|
'refresh-blocks'?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReplacedTransaction extends Transaction {
|
export interface ReplacedTransaction extends Transaction {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree } from '../interfaces/node-api.interface';
|
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||||
@ -72,6 +72,10 @@ export class ApiService {
|
|||||||
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y');
|
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listAllTimeStatistics$(): Observable<OptimizedMempoolStats[]> {
|
||||||
|
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/all');
|
||||||
|
}
|
||||||
|
|
||||||
getTransactionTimes$(txIds: string[]): Observable<number[]> {
|
getTransactionTimes$(txIds: string[]): Observable<number[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
txIds.forEach((txId: string) => {
|
txIds.forEach((txId: string) => {
|
||||||
@ -245,9 +249,9 @@ export class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockAudit$(hash: string) : Observable<any> {
|
getBlockAudit$(hash: string) : Observable<BlockAudit> {
|
||||||
return this.httpClient.get<any>(
|
return this.httpClient.get<BlockAudit>(
|
||||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`, { observe: 'response' }
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +235,8 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleResponse(response: WebsocketResponse) {
|
handleResponse(response: WebsocketResponse) {
|
||||||
|
let reinitBlocks = false;
|
||||||
|
|
||||||
if (response.blocks && response.blocks.length) {
|
if (response.blocks && response.blocks.length) {
|
||||||
const blocks = response.blocks;
|
const blocks = response.blocks;
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
@ -256,9 +258,11 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.block) {
|
if (response.block) {
|
||||||
if (response.block.height > this.stateService.latestBlockHeight) {
|
if (response.block.height === this.stateService.latestBlockHeight + 1) {
|
||||||
this.stateService.updateChainTip(response.block.height);
|
this.stateService.updateChainTip(response.block.height);
|
||||||
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
||||||
|
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
||||||
|
reinitBlocks = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.txConfirmed) {
|
if (response.txConfirmed) {
|
||||||
@ -369,5 +373,9 @@ export class WebsocketService {
|
|||||||
if (response['git-commit']) {
|
if (response['git-commit']) {
|
||||||
this.stateService.backendInfo$.next(response['git-commit']);
|
this.stateService.backendInfo$.next(response['git-commit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reinitBlocks) {
|
||||||
|
this.websocketSubject.next({'refresh-blocks': true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ do for url in / \
|
|||||||
'/api/v1/statistics/2y' \
|
'/api/v1/statistics/2y' \
|
||||||
'/api/v1/statistics/3y' \
|
'/api/v1/statistics/3y' \
|
||||||
'/api/v1/statistics/4y' \
|
'/api/v1/statistics/4y' \
|
||||||
|
'/api/v1/statistics/all' \
|
||||||
'/api/v1/mining/pools/24h' \
|
'/api/v1/mining/pools/24h' \
|
||||||
'/api/v1/mining/pools/3d' \
|
'/api/v1/mining/pools/3d' \
|
||||||
'/api/v1/mining/pools/1w' \
|
'/api/v1/mining/pools/1w' \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user