Compare commits
3 Commits
dependabot
...
natsoni/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6285dfa26 | ||
|
|
510b4adcea | ||
|
|
0928d64e1e |
@@ -42,7 +42,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
@@ -322,20 +321,6 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||
if (!transaction) {
|
||||
handleError(req, res, 404, `transaction not found in summary`);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(transaction);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlock(req: Request, res: Response) {
|
||||
try {
|
||||
const block = await blocks.$getBlock(req.params.hash);
|
||||
|
||||
@@ -412,16 +412,8 @@ class Blocks {
|
||||
}
|
||||
|
||||
try {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
const currentBlockHeight = blockchainInfo.blocks;
|
||||
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||
if (indexingBlockAmount <= -1) {
|
||||
indexingBlockAmount = currentBlockHeight + 1;
|
||||
}
|
||||
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||
|
||||
// Get all indexed block hash
|
||||
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
|
||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||
|
||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||
@@ -1224,11 +1216,6 @@ class Blocks {
|
||||
return summary.transactions;
|
||||
}
|
||||
|
||||
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
|
||||
const txs = await this.$getStrippedBlockTransactions(hash);
|
||||
return txs.find(tx => tx.txid === txid) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 15 blocks
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 93;
|
||||
private static currentVersion = 83;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -710,97 +710,6 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(83);
|
||||
}
|
||||
|
||||
// add new pools indexes
|
||||
if (databaseSchemaVersion < 84 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`pools\`
|
||||
ADD INDEX \`slug\` (\`slug\`),
|
||||
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(84);
|
||||
}
|
||||
|
||||
// lightning channels indexes
|
||||
if (databaseSchemaVersion < 85 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`channels\`
|
||||
ADD INDEX \`created\` (\`created\`),
|
||||
ADD INDEX \`capacity\` (\`capacity\`),
|
||||
ADD INDEX \`closing_reason\` (\`closing_reason\`),
|
||||
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(85);
|
||||
}
|
||||
|
||||
// lightning nodes indexes
|
||||
if (databaseSchemaVersion < 86 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`nodes\`
|
||||
ADD INDEX \`status\` (\`status\`),
|
||||
ADD INDEX \`channels\` (\`channels\`),
|
||||
ADD INDEX \`country_id\` (\`country_id\`),
|
||||
ADD INDEX \`as_number\` (\`as_number\`),
|
||||
ADD INDEX \`first_seen\` (\`first_seen\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(86);
|
||||
}
|
||||
|
||||
// lightning node sockets indexes
|
||||
if (databaseSchemaVersion < 87 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(87);
|
||||
}
|
||||
|
||||
// lightning stats indexes
|
||||
if (databaseSchemaVersion < 88 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||
await this.updateToSchemaVersion(88);
|
||||
}
|
||||
|
||||
// geo names indexes
|
||||
if (databaseSchemaVersion < 89 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||
await this.updateToSchemaVersion(89);
|
||||
}
|
||||
|
||||
// hashrates indexes
|
||||
if (databaseSchemaVersion < 90 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(90);
|
||||
}
|
||||
|
||||
// block audits indexes
|
||||
if (databaseSchemaVersion < 91 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||
await this.updateToSchemaVersion(91);
|
||||
}
|
||||
|
||||
// elements_pegs indexes
|
||||
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`elements_pegs\`
|
||||
ADD INDEX \`block\` (\`block\`),
|
||||
ADD INDEX \`datetime\` (\`datetime\`),
|
||||
ADD INDEX \`amount\` (\`amount\`),
|
||||
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
|
||||
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(92);
|
||||
}
|
||||
|
||||
// federation_txos indexes
|
||||
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`federation_txos\`
|
||||
ADD INDEX \`unspent\` (\`unspent\`),
|
||||
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
|
||||
ADD INDEX \`blocktime\` (\`blocktime\`),
|
||||
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
|
||||
ADD INDEX \`expiredAt\` (\`expiredAt\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(93);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,6 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
|
||||
@@ -208,7 +207,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -355,7 +354,7 @@ class Mempool {
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
|
||||
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
|
||||
if (accelerationDelta.length) {
|
||||
hasChange = true;
|
||||
}
|
||||
@@ -400,11 +399,58 @@ class Mempool {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
try {
|
||||
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||
const changed: string[] = [];
|
||||
|
||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
// skip transactions we don't know about
|
||||
if (!this.mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
newAccelerationMap[acceleration.txid] = acceleration;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (this.accelerations[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
this.accelerations[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
this.accelerations = newAccelerationMap;
|
||||
return accelerationDelta;
|
||||
|
||||
return changed;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
|
||||
@@ -459,7 +459,7 @@ class MiningRoutes {
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
} catch (e) {
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -136,13 +136,9 @@ class Mining {
|
||||
poolsStatistics['blockCount'] = blockCount;
|
||||
|
||||
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
||||
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
|
||||
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
||||
|
||||
try {
|
||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||
} catch (e) {
|
||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
import mempool from '../mempool';
|
||||
import websocketHandler from '../websocket-handler';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
@@ -40,23 +37,14 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
private ws: WebSocket | null = null;
|
||||
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private startedWebsocketLoop: boolean = false;
|
||||
private websocketConnected: boolean = false;
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
|
||||
private _accelerations: Record<string, Acceleration> = {};
|
||||
private _accelerations: Acceleration[] | null = null;
|
||||
private lastPoll = 0;
|
||||
private lastPing = Date.now();
|
||||
private lastPong = Date.now();
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public getAccelerations(): Record<string, Acceleration> {
|
||||
public get accelerations(): Acceleration[] | null {
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -84,18 +72,11 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||
if (this.useWebsocket && this.websocketConnected) {
|
||||
return this._accelerations;
|
||||
}
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
const latestAccelerations = {};
|
||||
for (const acc of accelerations) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
this._accelerations = accelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
} else {
|
||||
@@ -104,7 +85,7 @@ class AccelerationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||
@@ -139,11 +120,7 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
const latestAccelerations = {};
|
||||
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -175,148 +152,6 @@ class AccelerationApi {
|
||||
}
|
||||
return anyAccelerated;
|
||||
}
|
||||
|
||||
// get a list of accelerations that have changed between two sets of accelerations
|
||||
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
const changed: string[] = [];
|
||||
const mempoolCache = mempool.getMempool();
|
||||
|
||||
for (const acceleration of Object.values(newAccelerationMap)) {
|
||||
// skip transactions we don't know about
|
||||
if (!mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
if (oldAccelerationMap[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(oldAccelerationMap)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private handleWebsocketMessage(msg: any): void {
|
||||
if (msg?.accelerations !== null) {
|
||||
const latestAccelerations = {};
|
||||
for (const acc of msg?.accelerations || []) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
websocketHandler.handleAccelerationsChanged(this._accelerations);
|
||||
}
|
||||
}
|
||||
|
||||
public async connectWebsocket(): Promise<void> {
|
||||
if (this.startedWebsocketLoop) {
|
||||
return;
|
||||
}
|
||||
while (this.useWebsocket) {
|
||||
this.startedWebsocketLoop = true;
|
||||
if (!this.ws) {
|
||||
this.ws = new WebSocket(this.websocketPath);
|
||||
this.lastPing = 0;
|
||||
|
||||
this.ws.on('open', () => {
|
||||
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
|
||||
this.websocketConnected = true;
|
||||
this.ws?.send(JSON.stringify({
|
||||
'watch-accelerations': true
|
||||
}));
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
|
||||
if (error['errors']) {
|
||||
errMsg += ' - ' + error['errors'].join(' - ');
|
||||
}
|
||||
logger.err(errMsg);
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('close', () => {
|
||||
logger.info('Acceleration websocket closed');
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('message', (data, isBinary) => {
|
||||
try {
|
||||
const msg = (isBinary ? data : data.toString()) as string;
|
||||
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
|
||||
this.handleWebsocketMessage(parsedMsg);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('ping', () => {
|
||||
logger.debug('received ping from acceleration websocket server');
|
||||
});
|
||||
|
||||
this.ws.on('pong', () => {
|
||||
logger.debug('received pong from acceleration websocket server');
|
||||
this.lastPong = Date.now();
|
||||
});
|
||||
} else if (this.websocketConnected) {
|
||||
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
|
||||
logger.warn('No pong received within 10 seconds, terminating connection');
|
||||
try {
|
||||
this.ws?.terminate();
|
||||
} catch (e) {
|
||||
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
this.lastPing = 0;
|
||||
}
|
||||
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
|
||||
logger.debug('sending ping to acceleration websocket server');
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws?.ping();
|
||||
this.lastPing = Date.now();
|
||||
} catch (e) {
|
||||
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationApi();
|
||||
@@ -22,7 +22,6 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
|
||||
import Audit from './audit';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
@@ -61,8 +60,6 @@ class WebsocketHandler {
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
private mempoolSequence: number = 0;
|
||||
|
||||
private accelerations: Record<string, Acceleration> = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
addWebsocketServer(wss: WebSocket.Server) {
|
||||
@@ -498,42 +495,6 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server has been set');
|
||||
}
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
if (!websocketAccelerationDelta.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = JSON.stringify({
|
||||
accelerations: accelerationUpdate,
|
||||
});
|
||||
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleReorg(): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
@@ -610,7 +571,7 @@ class WebsocketHandler {
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = accelerationApi.getAccelerations();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
@@ -718,13 +679,10 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
|
||||
@@ -233,11 +233,11 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
if (config.WALLETS.ENABLED) {
|
||||
@@ -318,10 +318,8 @@ class Server {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
|
||||
accelerationApi.connectWebsocket();
|
||||
}
|
||||
|
||||
|
||||
setUpHttpApiRoutes(): void {
|
||||
bitcoinRoutes.initRoutes(this.app);
|
||||
bitcoinCoreRoutes.initRoutes(this.app);
|
||||
|
||||
@@ -501,7 +501,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
query += ` ORDER BY height DESC
|
||||
LIMIT 100`;
|
||||
LIMIT 10`;
|
||||
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(query, params);
|
||||
|
||||
32
frontend/package-lock.json
generated
32
frontend/package-lock.json
generated
@@ -9732,9 +9732,9 @@
|
||||
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9755,7 +9755,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@@ -9770,10 +9770,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
@@ -14059,9 +14055,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -25291,9 +25287,9 @@
|
||||
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -25314,7 +25310,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@@ -28493,9 +28489,9 @@
|
||||
}
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppModule } from '@app/app.module';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { ZoneService } from '@app/services/zone.service';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppRoutingModule } from '@app/app-routing.module';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { OrdApiService } from '@app/services/ord-api.service';
|
||||
|
||||
@@ -172,6 +172,10 @@
|
||||
background-color: var(--tertiary);
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { md5 } from '@app/shared/common.utils';
|
||||
import { md5, insecureRandomUUID } from '@app/shared/common.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ETA, EtaService } from '@app/services/eta.service';
|
||||
@@ -94,6 +94,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
estimateSubscription: Subscription;
|
||||
@@ -137,6 +138,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
|
||||
// Check if Apple Pay available
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
@@ -200,7 +202,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
this.processing = false;
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -386,6 +387,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
@@ -519,6 +521,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -618,6 +621,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -708,6 +712,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div
|
||||
#tooltip
|
||||
*ngIf="accelerationInfo && tooltipPosition !== null"
|
||||
class="acceleration-tooltip"
|
||||
[style.left]="tooltipPosition.x + 'px'"
|
||||
[style.top]="tooltipPosition.y + 'px'"
|
||||
[style.visibility]="accelerationInfo ? 'visible' : 'hidden'"
|
||||
[style.left]="tooltipPosition?.x + 'px'"
|
||||
[style.top]="tooltipPosition?.y + 'px'"
|
||||
>
|
||||
<table>
|
||||
<table *ngIf="accelerationInfo">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
|
||||
@@ -52,8 +52,7 @@
|
||||
class="pool-logo"
|
||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
<br *ngIf="i % 6 === 5">
|
||||
</ng-container>
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges, HostListener } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline-tooltip',
|
||||
@@ -10,6 +10,7 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
|
||||
tooltipPosition: any = null;
|
||||
yScroll = window.scrollY;
|
||||
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@@ -21,6 +22,9 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||
let y = changes.cursorPosition.currentValue.y + 20;
|
||||
if (this.tooltipElement) {
|
||||
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
||||
if (this.accelerationInfo?.status !== 'seen') {
|
||||
elementBounds.width = 370; // ugly hack to handle varying width due to pools logo loading
|
||||
}
|
||||
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
|
||||
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
|
||||
}
|
||||
@@ -35,4 +39,12 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||
hasPoolsData(): boolean {
|
||||
return Object.keys(this.accelerationInfo.poolsData).length > 0;
|
||||
}
|
||||
|
||||
@HostListener('window:scroll', ['$event'])
|
||||
onWindowScroll(): void {
|
||||
if (this.tooltipPosition) {
|
||||
this.tooltipPosition.y = this.tooltipPosition.y - (window.scrollY - this.yScroll);
|
||||
}
|
||||
this.yScroll = window.scrollY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
|
||||
<div class="shape"></div>
|
||||
<div class="shape-border hovering">
|
||||
<div id="step" class="shape" (pointerover)="onHover($event, 'seen');"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
@@ -78,8 +78,8 @@
|
||||
} @else {
|
||||
<div class="seen-to-acc right"></div>
|
||||
}
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||
<div class="shape"></div>
|
||||
<div class="shape-border hovering">
|
||||
<div id="step" class="shape" (pointerover)="onHover($event, 'accelerated');"></div>
|
||||
@if (!tx.status.confirmed) {
|
||||
<div class="connector down loading"></div>
|
||||
}
|
||||
@@ -111,11 +111,8 @@
|
||||
} @else {
|
||||
<div class="seen-to-acc left"></div>
|
||||
}
|
||||
<div class="shape-border"
|
||||
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
|
||||
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
|
||||
(pointerout)="onBlur($event);">
|
||||
<div class="shape"></div>
|
||||
<div class="shape-border" [ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}">
|
||||
<div id="step" class="shape" (pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)" ></div>
|
||||
</div>
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
|
||||
@@ -52,6 +52,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
onHover(event, status: string): void {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
if (status === 'seen') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
@@ -80,12 +81,19 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event): void {
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
if (event.target.id === 'step') {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
} else {
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
clickAway(event) {
|
||||
if (event.target.id !== 'step') {
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||
<div class="acceleration-list">
|
||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
@@ -21,8 +21,8 @@
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td colspan="3" class="pt-0">
|
||||
<div class="d-flex justify-content-end align-items-start">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { environment } from '@environments/environment';
|
||||
import { environment } from '@app/../environments/environment';
|
||||
import { AssetsService } from '@app/services/assets.service';
|
||||
import { moveDec } from '@app/bitcoin.utils';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
@@ -50,7 +49,6 @@ export class BlocksList implements OnInit {
|
||||
private ogService: OpenGraphService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||
@@ -184,7 +182,7 @@ export class BlocksList implements OnInit {
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
|
||||
this.router.navigate(['blocks', page]);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
|
||||
@@ -73,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||
];
|
||||
goggleFlags = 0n;
|
||||
goggleMode: FilterMode = 'and';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.sticky-loading {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
z-index: 99;
|
||||
font-size: 14px;
|
||||
@media (width >= 992px) {
|
||||
left: 32px;
|
||||
|
||||
@@ -267,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
if (event.key === prevKey) {
|
||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]);
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
const blocks = this.stateService.blocksSubject$.getValue();
|
||||
for (const block of (blocks || [])) {
|
||||
|
||||
@@ -90,9 +90,9 @@
|
||||
<th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
|
||||
<th class=""></th>
|
||||
<th class="" i18n="mining.pool-name">Pool</th>
|
||||
<th class="" *ngIf="['24h', '3d', '1w'].includes(this.miningWindowPreference)" i18n="mining.hashrate">Hashrate</th>
|
||||
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
|
||||
<th class="" i18n="master-page.blocks">Blocks</th>
|
||||
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}" i18n="latest-blocks.avg_health"
|
||||
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': this.miningWindowPreference === '24h'}" i18n="latest-blocks.avg_health"
|
||||
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
||||
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
|
||||
@@ -105,13 +105,12 @@
|
||||
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
|
||||
</td>
|
||||
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="d-flex justify-content-center">
|
||||
{{ pool.blockCount }}<span class="d-none d-md-table-cell"> ({{ pool.share }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': this.miningWindowPreference === '24h'}">
|
||||
<a
|
||||
class="health-badge badge"
|
||||
[class.badge-success]="pool.avgMatchRate >= 99"
|
||||
@@ -137,9 +136,8 @@
|
||||
<td class="d-none d-md-table-cell"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class=""><b i18n="mining.all-miners">All miners</b></td>
|
||||
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate| number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate | number: '1.2-2' }} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
|
||||
@@ -161,12 +161,9 @@ export class PoolRankingComponent implements OnInit {
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
const i = pool.blockCount.toString();
|
||||
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
|
||||
let hashrate = pool.lastEstimatedHashrate;
|
||||
if ('3d' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate3d; }
|
||||
if ('1w' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate1w; }
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
hashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||
pool.lastEstimatedHashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
@@ -203,10 +200,13 @@ export class PoolRankingComponent implements OnInit {
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
const i = totalBlockOther.toString();
|
||||
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + totalEstimatedHashrateOther.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
$localize`${ i }:INTERPOLATION: blocks`;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -292,8 +292,6 @@ export class PoolRankingComponent implements OnInit {
|
||||
getEmptyMiningStat(): MiningStats {
|
||||
return {
|
||||
lastEstimatedHashrate: 0,
|
||||
lastEstimatedHashrate3d: 0,
|
||||
lastEstimatedHashrate1w: 0,
|
||||
blockCount: 0,
|
||||
totalEmptyBlock: 0,
|
||||
totalEmptyBlockRatio: '',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ng-container *ngIf="(hosts$ | async) as hosts">
|
||||
<div class="status-panel">
|
||||
<table class="status-table table table-borderless table-striped">
|
||||
<table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="rank"></th>
|
||||
@@ -27,7 +27,7 @@
|
||||
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
|
||||
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { Observable, Subject, map, tap } from 'rxjs';
|
||||
import { Observable, Subject, map } from 'rxjs';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { HealthCheckHost } from '@interfaces/websocket.interface';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
@@ -13,7 +13,7 @@ import { DomSanitizer } from '@angular/platform-browser';
|
||||
})
|
||||
export class ServerHealthComponent implements OnInit {
|
||||
hosts$: Observable<HealthCheckHost[]>;
|
||||
maxHeight: number;
|
||||
tip$: Subject<number>;
|
||||
interval: number;
|
||||
now: number = Date.now();
|
||||
|
||||
@@ -44,14 +44,9 @@ export class ServerHealthComponent implements OnInit {
|
||||
host.flag = this.parseFlag(host.host);
|
||||
}
|
||||
return hosts;
|
||||
}),
|
||||
tap(hosts => {
|
||||
let newMaxHeight = 0;
|
||||
for (const host of hosts) {
|
||||
newMaxHeight = Math.max(newMaxHeight, host.latestHeight);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.tip$ = this.stateService.chainTip$;
|
||||
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
}
|
||||
</div>
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -280,7 +280,7 @@
|
||||
<ng-template #acceleratingRow>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="toggleCpfp()" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
|
||||
@@ -29,6 +29,7 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
@Input() hasEffectiveFeeRate: boolean;
|
||||
@Input() cpfpInfo: CpfpInfo;
|
||||
@Input() hasCpfp: boolean;
|
||||
@Input() showCpfpDetails: boolean;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() acceleratorAvailable: boolean;
|
||||
@Input() accelerateCtaType: string;
|
||||
@@ -50,7 +51,7 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
this.accelerateClicked.emit(true);
|
||||
}
|
||||
|
||||
toggleCpfp(): void {
|
||||
toggleCpfp(): void {
|
||||
this.toggleCpfp$.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
|
||||
[cpfpInfo]="cpfpInfo"
|
||||
[hasCpfp]="hasCpfp"
|
||||
[showCpfpDetails]="showCpfpDetails"
|
||||
[accelerationInfo]="accelerationInfo"
|
||||
[replaced]="replaced"
|
||||
[isCached]="isCached"
|
||||
@@ -68,9 +69,7 @@
|
||||
<!-- CPFP Details -->
|
||||
<ng-template [ngIf]="showCpfpDetails">
|
||||
<br>
|
||||
<div class="title">
|
||||
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
|
||||
</div>
|
||||
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
|
||||
<div class="box cpfp-details">
|
||||
<table class="table table-fixed table-borderless table-striped">
|
||||
<thead>
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
@@ -406,30 +406,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
|
||||
const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => {
|
||||
if (
|
||||
this.isFirstSeenAvailable(height)
|
||||
&& !audit?.firstSeen // firstSeen is not already in audit
|
||||
&& (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
|
||||
) {
|
||||
return useFullSummary ?
|
||||
this.apiService.getStrippedBlockTransactions$(hash).pipe(
|
||||
map(strippedTxs => {
|
||||
return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time };
|
||||
}),
|
||||
catchError(() => of({ audit }))
|
||||
) :
|
||||
this.apiService.getStrippedBlockTransaction$(hash, txid).pipe(
|
||||
map(strippedTx => {
|
||||
return { audit, firstSeen: strippedTx?.time };
|
||||
}),
|
||||
catchError(() => of({ audit }))
|
||||
);
|
||||
}
|
||||
return of({ audit });
|
||||
};
|
||||
|
||||
if (fetchAudit) {
|
||||
// If block audit is already cached, use it to get transaction audit
|
||||
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
||||
@@ -452,31 +428,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
}),
|
||||
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
|
||||
catchError(() => {
|
||||
return of({ audit: null });
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
|
||||
catchError(() => {
|
||||
return of({ audit: null });
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const audit = isCoinbase ? { coinbase: true } : null;
|
||||
return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash));
|
||||
return of(isCoinbase ? { coinbase: true } : null);
|
||||
}
|
||||
}),
|
||||
).subscribe(auditStatus => {
|
||||
this.auditStatus = auditStatus?.audit;
|
||||
const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
|
||||
if (firstSeen) {
|
||||
this.transactionTime = firstSeen;
|
||||
this.auditStatus = auditStatus;
|
||||
if (this.auditStatus?.firstSeen) {
|
||||
this.transactionTime = this.auditStatus.firstSeen;
|
||||
}
|
||||
this.setIsAccelerated();
|
||||
});
|
||||
@@ -953,11 +922,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'testnet4':
|
||||
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'signet':
|
||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||
return false;
|
||||
@@ -971,34 +935,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
isFirstSeenAvailable(blockHeight: number): boolean {
|
||||
if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||
return false;
|
||||
}
|
||||
switch (this.stateService.network) {
|
||||
case 'testnet':
|
||||
if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'testnet4':
|
||||
if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'signet':
|
||||
if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resetTransaction() {
|
||||
this.firstLoad = false;
|
||||
this.gotInitialPosition = false;
|
||||
|
||||
@@ -78,7 +78,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||
];
|
||||
goggleFlags = 0n;
|
||||
goggleMode: FilterMode = 'and';
|
||||
|
||||
@@ -9339,7 +9339,7 @@ export const restApiDocsData = [
|
||||
fragment: "accelerator-history",
|
||||
title: "GET Acceleration History",
|
||||
description: {
|
||||
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code> (required): <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code>.<br>Pass <code>true</code> in <code>:details</code> to get a detailed <code>history</code> of the acceleration request.</p>"
|
||||
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code>. Pass <code>true</code> in <code>:details</code> to get a detailed <code>history</code> of the acceleration request.</p>"
|
||||
},
|
||||
urlString: "/v1/services/accelerator/history?status=:status&details=:details",
|
||||
showConditions: [""],
|
||||
@@ -9449,36 +9449,6 @@ export const restApiDocsData = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
options: { officialOnly: true },
|
||||
type: "endpoint",
|
||||
category: "accelerator-private",
|
||||
httpRequestMethod: "POST",
|
||||
fragment: "accelerator-cancel",
|
||||
title: "POST Cancel Acceleration (Pro)",
|
||||
description: {
|
||||
default: "<p>Sends a request to cancel an acceleration in the <code>accelerating</code> status.<br>You can retreive eligible acceleration <code>id</code> using the history endpoint GET <code>/api/v1/services/accelerator/history?status=accelerating</code>."
|
||||
},
|
||||
urlString: "/v1/services/accelerator/cancel",
|
||||
showConditions: [""],
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/accelerator/cancel`, //custom interpolation technique handled in replaceCurlPlaceholder()
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["id=42"],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `HTTP/1.1 200 OK`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const faqData = [
|
||||
|
||||
@@ -143,8 +143,6 @@ export interface SinglePoolStats {
|
||||
rank: number;
|
||||
share: number;
|
||||
lastEstimatedHashrate: number;
|
||||
lastEstimatedHashrate3d: number;
|
||||
lastEstimatedHashrate1w: number;
|
||||
emptyBlockRatio: string;
|
||||
logo: string;
|
||||
slug: string;
|
||||
@@ -154,8 +152,6 @@ export interface SinglePoolStats {
|
||||
export interface PoolsStats {
|
||||
blockCount: number;
|
||||
lastEstimatedHashrate: number;
|
||||
lastEstimatedHashrate3d: number;
|
||||
lastEstimatedHashrate1w: number;
|
||||
pools: SinglePoolStats[];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GraphsModule } from '@app/graphs/graphs.module';
|
||||
import { PreviewsRoutingModule } from './previews.routing.module';
|
||||
import { PreviewsRoutingModule } from '@app/previews.routing.module';
|
||||
import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component';
|
||||
import { BlockPreviewComponent } from '@components/block/block-preview.component';
|
||||
import { AddressPreviewComponent } from '@components/address/address-preview.component';
|
||||
|
||||
@@ -18,7 +18,6 @@ export class ApiService {
|
||||
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
public blockSummaryLoaded: { [hash: string]: boolean } = {};
|
||||
public blockAuditLoaded: { [hash: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
@@ -319,14 +318,9 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
|
||||
this.setBlockSummaryLoaded(hash);
|
||||
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
|
||||
}
|
||||
|
||||
getStrippedBlockTransaction$(hash: string, txid: string): Observable<TransactionStripped> {
|
||||
return this.httpClient.get<TransactionStripped>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary');
|
||||
}
|
||||
|
||||
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
|
||||
@@ -573,12 +567,4 @@ export class ApiService {
|
||||
getBlockAuditLoaded(hash) {
|
||||
return this.blockAuditLoaded[hash];
|
||||
}
|
||||
|
||||
async setBlockSummaryLoaded(hash: string) {
|
||||
this.blockSummaryLoaded[hash] = true;
|
||||
}
|
||||
|
||||
getBlockSummaryLoaded(hash) {
|
||||
return this.blockSummaryLoaded[hash];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ export interface MiningUnits {
|
||||
|
||||
export interface MiningStats {
|
||||
lastEstimatedHashrate: number;
|
||||
lastEstimatedHashrate3d: number;
|
||||
lastEstimatedHashrate1w: number;
|
||||
blockCount: number;
|
||||
totalEmptyBlock: number;
|
||||
totalEmptyBlockRatio: string;
|
||||
@@ -131,8 +129,6 @@ export class MiningService {
|
||||
return {
|
||||
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
||||
lastEstimatedHashrate: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider,
|
||||
lastEstimatedHashrate3d: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate3d / hashrateDivider,
|
||||
lastEstimatedHashrate1w: poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate1w / hashrateDivider,
|
||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
||||
...poolStat
|
||||
@@ -141,8 +137,6 @@ export class MiningService {
|
||||
|
||||
return {
|
||||
lastEstimatedHashrate: stats.lastEstimatedHashrate / hashrateDivider,
|
||||
lastEstimatedHashrate3d: stats.lastEstimatedHashrate3d / hashrateDivider,
|
||||
lastEstimatedHashrate1w: stats.lastEstimatedHashrate1w / hashrateDivider,
|
||||
blockCount: stats.blockCount,
|
||||
totalEmptyBlock: totalEmptyBlock,
|
||||
totalEmptyBlockRatio: totalEmptyBlockRatio,
|
||||
|
||||
@@ -131,20 +131,20 @@ export class ServicesApiServices {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
|
||||
}
|
||||
|
||||
accelerate$(txInput: string, userBid: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid});
|
||||
accelerate$(txInput: string, userBid: number, accelerationUUID: string) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
|
||||
}
|
||||
|
||||
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
getAccelerations$(): Observable<Acceleration[]> {
|
||||
|
||||
@@ -68,12 +68,7 @@ export interface Env {
|
||||
AUDIT: boolean;
|
||||
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET4_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
MAINNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
TESTNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
SIGNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
HISTORICAL_PRICE: boolean;
|
||||
ACCELERATOR: boolean;
|
||||
ACCELERATOR_BUTTON: boolean;
|
||||
@@ -112,12 +107,7 @@ const defaultEnv: Env = {
|
||||
'AUDIT': false,
|
||||
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'HISTORICAL_PRICE': true,
|
||||
'ACCELERATOR': false,
|
||||
'ACCELERATOR_BUTTON': true,
|
||||
|
||||
@@ -214,6 +214,19 @@ export function renderSats(value: number, network: string, mode: 'sats' | 'btc'
|
||||
}
|
||||
}
|
||||
|
||||
export function insecureRandomUUID(): string {
|
||||
const hexDigits = '0123456789abcdef';
|
||||
const uuidLengths = [8, 4, 4, 4, 12];
|
||||
let uuid = '';
|
||||
for (const length of uuidLengths) {
|
||||
for (let i = 0; i < length; i++) {
|
||||
uuid += hexDigits[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
uuid += '-';
|
||||
}
|
||||
return uuid.slice(0, -1);
|
||||
}
|
||||
|
||||
export function sleep$(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1403,8 +1403,4 @@ a {
|
||||
color: var(--fg);
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
@@ -2014,9 +2014,9 @@ case $OS in
|
||||
moused_nondefault_enable="NO"
|
||||
|
||||
nginx_enable="YES"
|
||||
#nginx_profiles="mempool"
|
||||
#nginx_mempool_flags="-p /mempool"
|
||||
#nginx_mempool_configfile="/mempool/mempool.space/nginx/nginx.conf"
|
||||
nginx_profiles="mempool"
|
||||
nginx_mempool_flags="-p /mempool"
|
||||
nginx_mempool_configfile="/mempool/mempool/nginx/nginx.conf"
|
||||
|
||||
mysql_enable="YES"
|
||||
mysql_dbdir="/mysql"
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
|
||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
|
||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
|
||||
"MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316,
|
||||
"ITEMS_PER_PAGE": 25,
|
||||
"LIGHTNING": true,
|
||||
"ACCELERATOR": true,
|
||||
|
||||
@@ -55,10 +55,10 @@ location /api/block/ {
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-api-cache-forever;
|
||||
}
|
||||
# we cache for 1s to mitigate DoS attacks
|
||||
# other API responses cannot be cached
|
||||
location /api/ {
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-api-cache-minimal;
|
||||
try_files /dev/null @esplora-api-cache-disabled;
|
||||
}
|
||||
|
||||
###########
|
||||
@@ -171,23 +171,6 @@ location @esplora-api-cache-disabled {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @esplora-api-cache-minimal {
|
||||
proxy_pass $esploraMainnet;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache apihot;
|
||||
proxy_cache_valid 200 1s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 1s;
|
||||
}
|
||||
|
||||
location @esplora-api-cache-forever {
|
||||
proxy_pass $esploraMainnet;
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ location /testnet/api/block/ {
|
||||
rewrite ^/testnet/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-testnet-api-cache-forever;
|
||||
}
|
||||
# we cache for 1s to mitigate DoS attacks
|
||||
# other API responses cannot be cached
|
||||
location /testnet/api/ {
|
||||
rewrite ^/testnet/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-testnet-api-cache-minimal;
|
||||
try_files /dev/null @esplora-testnet-api-cache-disabled;
|
||||
}
|
||||
|
||||
###########
|
||||
@@ -160,20 +160,3 @@ location @esplora-testnet-api-cache-forever {
|
||||
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location @esplora-testnet-api-cache-minimal {
|
||||
proxy_pass $esploraTestnet;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_cache_background_update off;
|
||||
proxy_cache_use_stale error;
|
||||
proxy_cache apihot;
|
||||
proxy_cache_valid 200 1s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 1s;
|
||||
}
|
||||
|
||||
@@ -42,9 +42,6 @@ http {
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# set cors headers if necessary
|
||||
set $cors_approved_origin '';
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:81;
|
||||
set $onion "__NGINX_MEMPOOL_ONION__";
|
||||
@@ -83,9 +80,6 @@ http {
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# set cors headers if necessary
|
||||
set $cors_approved_origin '';
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:83;
|
||||
set $onion "__NGINX_LIQUID_ONION__";
|
||||
|
||||
Reference in New Issue
Block a user